W3docs

Прототипное наследование в JavaScript

Прототипное наследование JavaScript: скрытая связь [[Prototype]], __proto__ против Object.getPrototypeOf/setPrototypeOf, цепочка прототипов, F.prototype и this.

В JavaScript объекты могут наследовать свойства и методы от других объектов через механизм, называемый прототипным наследованием. Вместо копирования функциональности из класса (как в Java или C++), каждый object содержит скрытую ссылку на другой object — свой прототип — и JavaScript следует по этой ссылке всякий раз, когда не может найти свойство на самом object. На этой странице рассматривается работа скрытой ссылки, разница между __proto__ и Object.getPrototypeOf/setPrototypeOf, способ поиска по цепочке прототипов, роль F.prototype для функций-конструкторов, затенение свойств и поведение this вдоль цепочки.

Скрытая ссылка [[Prototype]]

Каждый JavaScript object имеет скрытое внутреннее свойство [[Prototype]]. Оно может быть либо null, либо ссылкой на другой object, который и называется прототипом этого object.

[[Prototype]] — это внутренний слот в спецификации языка: его нельзя прочитать с помощью обычного обращения к свойству. Для работы с ним используются два публичных API:

  • Исторический аксессор __proto__ (геттер/сеттер, предоставляемый Object.prototype).
  • Современные, рекомендуемые методы Object.getPrototypeOf(obj) и Object.setPrototypeOf(obj, proto).

Самый простой способ задать прототип — использовать __proto__ внутри литерала object. Здесь мы делаем animal прототипом rabbit, чтобы rabbit мог читать свойства animal:


javascript— editable

__proto__ vs Object.getPrototypeOf / setPrototypeOf

__proto__ — это геттер/сеттер, который давно устарел для общего использования: он стандартизирован лишь для совместимости с браузерами. В реальном коде предпочитайте явные методы:


javascript— editable

Обратите внимание на разницу: __proto__ — это аксессор свойства, тогда как getPrototypeOf/setPrototypeOf — это функции. Два практических правила:

  • __proto__ — это не то же самое, что [[Prototype]]. __proto__ — лишь аксессор, который читает и записывает внутренний слот [[Prototype]].
  • Избегайте изменения прототипа после создания object. Операции Object.setPrototypeOf и присвоение obj.__proto__ выполняются медленно: движки активно оптимизируют объекты, чей прототип зафиксирован в момент создания. Задавайте прототип один раз — при создании object.

Цепочка прототипов

Прототип прототипа тоже может иметь прототип, образуя цепочку прототипов. При чтении obj.prop JavaScript:

  1. Ищет prop как собственное свойство obj.
  2. Если не найдено, следует по [[Prototype]] и ищет в прототипе.
  3. Повторяет шаг 2 вверх по цепочке, пока не найдёт prop или не достигнет null.

Если цепочка заканчивается на null, а свойство так и не найдено, результатом будет undefined.


javascript— editable

Существуют два ограничения цепочки: ссылки не должны образовывать цикл (JavaScript выбросит исключение при попытке создать кольцо), а [[Prototype]] должен быть либо object, либо null.

Затенение свойств: запись vs. чтение

Прототип используется только при чтении. При записи или удалении свойства операция всегда выполняется над самим object, но никогда над его прототипом. Присвоение свойству, которое уже существует в прототипе, создаёт собственное свойство, затеняющее (скрывающее) унаследованное:


javascript— editable

Исключение составляют аксессорные свойства (геттеры/сеттеры): поскольку сеттер является вызовом функции, запись через унаследованный сеттер запускает этот сеттер, а не создаёт новое собственное свойство-данные.

this — всегда вызывающий object

Распространённый источник путаницы: вне зависимости от того, где в цепочке находится метод, значение this внутри него — это object перед точкой в момент вызова метода, а не прототип, в котором он определён. Поэтому унаследованные методы работают с собственным состоянием наследующего object:


javascript— editable

Именно это делает совместные методы на прототипе полезными: одно определение метода, но каждый object хранит собственные данные.

Функции-конструкторы и F.prototype

Задавать [[Prototype]] вручную для каждого object утомительно. Классический паттерн — функция-конструктор, используемая с new. У каждой функции есть обычное свойство prototype (обозначается F.prototype). При вызове new F() [[Prototype]] только что созданного object устанавливается равным F.prototype.


javascript— editable

Важно различать F.prototype и [[Prototype]] object: F.prototype — это обычное свойство функции-конструктора, которое служит [[Prototype]] для объектов, созданных с помощью new F(). По умолчанию F.prototype — это object с единственным не перечисляемым свойством constructor, указывающим обратно на саму функцию.


javascript— editable

Примечание: Синтаксис class в ES6 — это синтаксический сахар поверх этого же механизма. Объявление class создаёт функцию-конструктор, помещает её методы в Constructor.prototype и выстраивает цепочку с теми же ссылками [[Prototype]]. Подробнее о форме extends/super см. в разделе Наследование классов JavaScript.

Создание объектов с помощью Object.create

Object.create(proto) создаёт новый object с [[Prototype]], установленным непосредственно в proto — конструктор не нужен. Это наиболее явный способ настройки наследования; он принимает второй аргумент — карту дескрипторов свойств в том же формате, который используется Object.defineProperties.


javascript— editable

Карта дескрипторов позволяет управлять флагами writable, enumerable и configurable — о назначении каждого флага см. в разделе Флаги и дескрипторы свойств JavaScript. Объекты, созданные с помощью Object.create(null) (так называемые «чистые» объекты), рассматриваются в разделе Методы прототипов, объекты без __proto__.

Многоуровневое наследование

Поскольку каждый прототип может иметь собственный прототип, можно выстраивать цепочки в несколько уровней для моделирования более специфических отношений:


javascript— editable

Инспектирование и перебор цепочки

Чтобы проверить, находится ли object где-то в цепочке другого object, используйте isPrototypeOf. Чтобы разграничить собственные и унаследованные свойства, используйте hasOwnProperty — учтите, что for...in проходит по всей цепочке (только перечисляемые свойства), тогда как Object.keys возвращает только собственные ключи.


javascript— editable

Встроенные типы, такие как arrays, функции и даты, также опираются на эту цепочку — их методы живут на Array.prototype, Function.prototype и так далее. О том, как выстроены встроенные типы, см. в разделе Встроенные прототипы JavaScript.

Резюме

  • Каждый object имеет скрытый [[Prototype]], являющийся другим object или null.
  • Читайте [[Prototype]] с помощью Object.getPrototypeOf, устанавливайте с помощью Object.setPrototypeOf; __proto__ — устаревший аксессор, и изменение прототипа после создания object выполняется медленно.
  • Чтение проходит вверх по цепочке прототипов до тех пор, пока свойство не найдено или цепочка не заканчивается на null; запись и удаление всегда выполняются над самим object и могут затенять унаследованные свойства.
  • this внутри метода — это object, на котором вызван метод, а не прототип, в котором он определён.
  • new F() устанавливает [[Prototype]] нового object равным F.prototype; class в ES6 — синтаксический сахар поверх этого механизма — см. Наследование классов JavaScript.

Практика

Практика
Какие утверждения точно описывают прототипное наследование в JavaScript?
Какие утверждения точно описывают прототипное наследование в JavaScript?
Was this page helpful?