Прототипы
1. Прототип объекта
ООП в JavaScript построено на прототипном наследовании. Объекты можно организовать в цепочки так, чтобы свойство, не найденное в одном объекте, автоматически искалось бы в другом. Связующим звеном выступает специальное скрытое свойство [[Prototype]], в консоли оно отображается как __proto__.
1.1. Прототип
Если один объект имеет специальную ссылку в поле __proto__ на другой объект, то при чтении свойства из него, если искомое свойство отсутствует в самом объекте, оно ищется в объекте на который ссылается __proto__.
const animal = { eats: true };
const dog = { barks: true };
dog.__proto__ = animal;
// В dog можно найти оба свойства
console.log(dog.barks); // true
console.log(dog.eats); // true
CopyПервый лог работает очевидным образом — он выводит свойство barks объекта dog. Второй лог хочет вывести dog.eats, ищет его в самом объекте dog, не находит и продолжает поиск в объекте по ссылке из dog.__proto__, то есть, в данном случае, в animal.
Объект, на который указывает ссылка в __proto__, называется прототипом. В данном примере animal - это прототип для dog.
Если мы добавим объекту dog свойство eats и присвоим ему false, то результат будет следующим.
const animal = { eats: true };
const dog = { barks: true, eats: false };
dog.__proto__ = animal;
console.log(dog.barks); // true
console.log(dog.eats); // false, свойство взято из dog
CopyДругими словами, прототип — это резервное хранилище свойств и методов объекта, автоматически используемое при поиске. У объекта, который выступает прототипом может также быть свой прототип, у того свой, и так далее. При этом свойства будут искаться по цепочке наследования.
В спецификации свойство __proto__ обозначено как [[Prototype]]. Двойные квадратные скобки здесь важны, они указывают на то, что это внутреннее, служебное свойство.
Несмотря на то, что свойство __proto__ на сегодняшний день стандартизовано и проще для объяснения материала, его изменение напрямую является грубым нарушением. На практике используются методы для манипуляции с прототипами, такие как Object.create(), Object.getPrototypeOf(), Object.setPrototypeOf() и другие.
1.2. Object.create()
Для того чтобы правильно задать прототип объекта, можно использовать метод Object.create(obj), передав параметром obj ссылку на объект, который мы хотим сделать прототипом для создаваемого объекта.
На рисунке ниже видно то, что называется цепочкой прототипов (prototype chain). В свойство __proto__ объекта dog записана ссылка на объект animal, в свойство __proto__ которого, в свою очередь, записана ссылка на родителя всех объектов Object. Именно поэтому мы можем вызывать методы вроде hasOwnProperty или toString, хотя мы не определяли их для dog или animal.

Механизм поиска свойства работает до первого совпадения. Интерпретатор ищет свойство по имени в объекте, если не находит, то обращается к свойству [[Prototype]], т.е. переходит по ссылке к объекту-прототипу, а затем и прототипу прототипа.
В конце этой цепочки находится null. В случае первого совпадения будет возвращено значение свойства. Если интерпретатор доберется до конца цепочки и не найдет свойства с таким ключом, то вернет undefined.

Данный механизм называется динамическая диспетчеризация (dynamic dispatch) или делегация (delegation). В отличие от статической диспетчеризации, когда ссылки разрешаются во время компиляции, динамическая диспетчеризация всегда разрешает ссылки во время исполнения программы.
1.3. Object.prototype.hasOwnProperty()
После того как мы узнали о том, как происходит поиск свойств объекта, должно стать понятно, почему цикл for...in не делает различия между свойствами объекта и его прототипа.
Именно поэтому мы используем метод obj.hasOwnProperty(prop), который возвращает true, если свойство prop принадлежит самому объекту obj, а не его прототипу, иначе false.
Метод Object.keys(obj) вернет массив только собственных ключей объекта obj, поэтому рекомендуется использовать именно его.
2. Свойство Function.prototype
Мы уже знаем, что такое прототип объекта, свойство __proto__, как происходит поиск отсутствующих свойств объекта по цепочке и методы указания прототипа для одного объекта. Но в реальных задачах объекты создаются динамически, то есть функцией-конструктором, через new. Давайте разберемся как происходит указание прототипа в этом случае.
Создадим функцию-конструктор Guest, которая будет создавать нам экземпляры объектов гостя отеля.
Так как функция - это тоже объект, у каждой функции, кроме стрелочных, есть свойство prototype в котором изначально хранится объект с единственным полем constructor, указывающим на саму функцию-конструктор.
Свойство Function.prototype:
Является объектом
В него можно записывать свойства и методы
Свойства и методы
prototypeбудут доступны по ссылке__proto__объектаУ свойства
prototypeизначально есть методconstructor
При создании объекта через new в его поле __proto__ записывается ссылка на объект, хранящийся в свойстве prototype функции-конструктора.


Эту особенность мы можем использовать для того, чтобы добавлять в объект prototype методы, которые будут доступны по ссылке абсолютно всем объектам, созданным через new Guest(...).
Причем если мы создадим миллион экземпляров гостя, набор методов будет не у каждого свой, а всего один, общий, хранящийся в свойстве Guest.prototype и доступный всем потомкам по ссылке, которая записывается в поле __proto__ объекта при создании из-за прототипного наследования и цепочки прототипов.

Так как в свойстве prototype лежит объект, то при прототипном наследование происходит присвоение по ссылке, поэтому если мы изменим значение у свойства prototype, то это новое значение получат и все свойства, имеющие ссылку на объект prototype.
2.1. Свойство constructor
Было упоминание того, что по умолчанию, объект в свойстве prototype уже содержит поле constructor. Запишем это поле явно:
В коде выше мы создали свойство Guest.prototype вручную, но абсолютно такой же объект генерируется автоматически. А свойство constructor содержит ссылку на саму функцию-конструктор.

Свойство [[Prototype]] объекта называют скрытым свойством, потому что прямой доступ к нему ограничен средствами самого языка. Метод, который делает запись ссылки в объект в момент создания имеет такой доступ. Находится этот метод в объекте prototype функции и называется constructor 🙂.
Understanding Prototypes in JavaScript
3. Наследование и конструкторы
Конструктор - это функция для создания объектов по шаблону. Оператор new создает объект и вызывает функцию-конструктор в контексте этого объекта. На выходе получаем объект с полями указанными в функции-конструкторе через this и полем [[Prototype]], которое содержит ссылку на поле prototype функции-конструктора.
Иногда случаются задачи, когда объекты, созданные функцией-конструктором, должны также иметь доступ к полям и методам прототипа объявленным в другой функции-конструкторе.
Например мы пишем игру в стиле RPG, и нам необходимо подготовить логику для классовой системы персонажей где есть общий конструктор Hero с дефолтными полями общими для всех классов, вроде имени, здоровья, количества опыта и т. п. После чего нам необходимо сделать конструкторы для Warrior и Wizard, экземпляры которых также должны иметь доступ к полям Hero, но в тоже время иметь свои собственные.
Давайте реализуем это используя прототипное наследование.
Далее необходимо создать класс Warrior, так как нет смысла добавлять в Hero абсолютно все поля всех классов. Поэтому нам необходимо создать еще функцию-конструктор, но при этом она должна быть как-то связана с Hero.
Для решения этой задачи мы можем использовать метод call(), вызвав функцию-конструктор Hero и передав ей объект, создающийся в Warrior как контекст.
Вроде все хорошо, но что произойдет если мы попробуем вызвать у poly метод gainXp(), который объявлен на Hero.prototype? — будет ошибка
Дело в том, что поля из Hero.prototype не добавляются в цепочку прототипов по умолчанию. Необходимо явно указать связь поля Warrior.prototype и Hero.prototype. Сделать это очень легко, но важно понимать как и почему это работает.
В результате мы получили цепочку прототипов. При вызове poly.gainXp(), идет поиск поля gainXp в самом объекте poly, если такового нет, тогда идет поиск в том объекте, который указан в поле poly.__proto__ - это ссылка на Warrior.prototype.
Если же его нету и там, то поиск идет в поле __proto__ того объекта, что указан в poly.__proto__, то есть в poly.__proto__.__proto__, а это ссылка на Hero.prototype, где есть метод gainXp.
Полный код примера.
4. Дополнительные материалы
Last updated