Прототипы
1. Прототип объекта
ООП в JavaScript построено на прототипном наследовании. Объекты можно организовать в цепочки так, чтобы свойство, не найденное в одном объекте, автоматически искалось бы в другом. Связующим звеном выступает специальное скрытое свойство [[Prototype]]
, в консоли оно отображается как __proto__
.
1.1. Прототип
Если один объект имеет специальную ссылку в поле __proto__
на другой объект, то при чтении свойства из него, если искомое свойство отсутствует в самом объекте, оно ищется в объекте на который ссылается __proto__
.
Первый лог работает очевидным образом — он выводит свойство barks
объекта dog
. Второй лог хочет вывести dog.eats
, ищет его в самом объекте dog
, не находит и продолжает поиск в объекте по ссылке из dog.__proto__
, то есть, в данном случае, в animal
.
Объект, на который указывает ссылка в __proto__
, называется прототипом. В данном примере animal
- это прототип для dog
.
Если мы добавим объекту dog
свойство eats
и присвоим ему false
, то результат будет следующим.
Другими словами, прототип — это резервное хранилище свойств и методов объекта, автоматически используемое при поиске. У объекта, который выступает прототипом может также быть свой прототип, у того свой, и так далее. При этом свойства будут искаться по цепочке наследования.
В спецификации свойство __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
🙂.
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