Прототипное наследование в 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:
__proto__ vs Object.getPrototypeOf / setPrototypeOf
__proto__ — это геттер/сеттер, который давно устарел для общего использования: он стандартизирован лишь для совместимости с браузерами. В реальном коде предпочитайте явные методы:
Обратите внимание на разницу: __proto__ — это аксессор свойства, тогда как getPrototypeOf/setPrototypeOf — это функции. Два практических правила:
__proto__— это не то же самое, что[[Prototype]].__proto__— лишь аксессор, который читает и записывает внутренний слот[[Prototype]].- Избегайте изменения прототипа после создания object. Операции
Object.setPrototypeOfи присвоениеobj.__proto__выполняются медленно: движки активно оптимизируют объекты, чей прототип зафиксирован в момент создания. Задавайте прототип один раз — при создании object.
Цепочка прототипов
Прототип прототипа тоже может иметь прототип, образуя цепочку прототипов. При чтении obj.prop JavaScript:
- Ищет
propкак собственное свойствоobj. - Если не найдено, следует по
[[Prototype]]и ищет в прототипе. - Повторяет шаг 2 вверх по цепочке, пока не найдёт
propили не достигнетnull.
Если цепочка заканчивается на null, а свойство так и не найдено, результатом будет undefined.
Существуют два ограничения цепочки: ссылки не должны образовывать цикл (JavaScript выбросит исключение при попытке создать кольцо), а [[Prototype]] должен быть либо object, либо null.
Затенение свойств: запись vs. чтение
Прототип используется только при чтении. При записи или удалении свойства операция всегда выполняется над самим object, но никогда над его прототипом. Присвоение свойству, которое уже существует в прототипе, создаёт собственное свойство, затеняющее (скрывающее) унаследованное:
Исключение составляют аксессорные свойства (геттеры/сеттеры): поскольку сеттер является вызовом функции, запись через унаследованный сеттер запускает этот сеттер, а не создаёт новое собственное свойство-данные.
this — всегда вызывающий object
Распространённый источник путаницы: вне зависимости от того, где в цепочке находится метод, значение this внутри него — это object перед точкой в момент вызова метода, а не прототип, в котором он определён. Поэтому унаследованные методы работают с собственным состоянием наследующего object:
Именно это делает совместные методы на прототипе полезными: одно определение метода, но каждый object хранит собственные данные.
Функции-конструкторы и F.prototype
Задавать [[Prototype]] вручную для каждого object утомительно. Классический паттерн — функция-конструктор, используемая с new. У каждой функции есть обычное свойство prototype (обозначается F.prototype). При вызове new F() [[Prototype]] только что созданного object устанавливается равным F.prototype.
Важно различать F.prototype и [[Prototype]] object: F.prototype — это обычное свойство функции-конструктора, которое служит [[Prototype]] для объектов, созданных с помощью new F(). По умолчанию F.prototype — это object с единственным не перечисляемым свойством constructor, указывающим обратно на саму функцию.
Примечание: Синтаксис
classв ES6 — это синтаксический сахар поверх этого же механизма. Объявлениеclassсоздаёт функцию-конструктор, помещает её методы вConstructor.prototypeи выстраивает цепочку с теми же ссылками[[Prototype]]. Подробнее о формеextends/superсм. в разделе Наследование классов JavaScript.
Создание объектов с помощью Object.create
Object.create(proto) создаёт новый object с [[Prototype]], установленным непосредственно в proto — конструктор не нужен. Это наиболее явный способ настройки наследования; он принимает второй аргумент — карту дескрипторов свойств в том же формате, который используется Object.defineProperties.
Карта дескрипторов позволяет управлять флагами writable, enumerable и configurable — о назначении каждого флага см. в разделе Флаги и дескрипторы свойств JavaScript. Объекты, созданные с помощью Object.create(null) (так называемые «чистые» объекты), рассматриваются в разделе Методы прототипов, объекты без __proto__.
Многоуровневое наследование
Поскольку каждый прототип может иметь собственный прототип, можно выстраивать цепочки в несколько уровней для моделирования более специфических отношений:
Инспектирование и перебор цепочки
Чтобы проверить, находится ли object где-то в цепочке другого object, используйте isPrototypeOf. Чтобы разграничить собственные и унаследованные свойства, используйте hasOwnProperty — учтите, что for...in проходит по всей цепочке (только перечисляемые свойства), тогда как Object.keys возвращает только собственные ключи.
Встроенные типы, такие как 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.