Эта статья дополняет первые 40 минут следующей видеолекции о различных концепциях ООП в JavaScript: https://www.youtube.com/watch?v=-N9tBvlO9Bo.

Видеолекция и содержание этой статьи основаны на концепциях, изложенных в JS 120 и JS 225 в рамках учебной программы Launch School по основам разработки программного обеспечения.

Пример 1

Мы начнем с определения простого литерала объекта obj в строках 2–5. Мы можем подтвердить, что он действительно имеет тип Object, используя typeof, и подтвердить, что он имеет свойства, которые мы для него определили, а именно, color и amount, используя метод hasOwnProperty.

Теперь первый вопрос, который возникает: как obj вообще получил доступ к методу hasOwnProperty? Когда мы вручную создали obj в строках 2–5, мы определили для него только два свойства; тем не менее, мы смогли вызвать hasOwnProperty, как если бы он был определен в obj.

На самом деле, мы можем вызвать этот самый метод, чтобы убедиться, что он не исходит от obj, т. е. obj.hasOwnProperty('hasOwnProperty') регистрирует false (см. строку 14 в фрагменте кода).

Ответ на этот вопрос содержится в схеме, показанной на рис. 1, который служит нашей первой моделью того, как наследование работает в JavaScript.

Как показано на схеме, поскольку мы вручную создали obj, используя синтаксис литерала объекта, за кулисами JavaScript использует конструктор Object для создания нового объекта. Более того, он устанавливает свойство [[Prototype]] элемента obj для ссылки на объект-прототип конструктора, Object.prototype. Именно от этого объекта obj наследует метод hasOwnProperty. В приведенном выше фрагменте кода (строка 24) это можно проверить, записав в журнал Object.prototype.hasOwnProperty('hasOwnProperty'), который оценивается как true.

Свойство [[Prototype]] — это скрытое свойство, фиксирующее связь между литералом объекта obj и его непосредственным родителем в цепочке прототипов. Почти все объекты в JavaScript имеют свойство [[Prototype]]. Он скрыт в том смысле, что к нему можно получить доступ только косвенно, используя либо статический метод Object.getPrototypeOf() (см. строку 19), либо свойство .__proto__ (см. строку 20), хотя последний подход теперь устарел и не рекомендуется для использования в производственной среде. code» (в этой статье мы по-прежнему будем использовать .__proto__ как аналог [[Prototype]]).

Так что это решает загадку источника hasOwnProperty.

Далее мы смотрим на другое свойство, constructor. Это свойство позволяет нам проверить происхождение данного объекта, т. е. как он был создан и представляет ли он данные определенного типа. Поскольку obj был создан вручную с использованием синтаксиса литерала объекта, его конструктором будет Object. Как кратко упоминалось ранее, Object — это встроенная функция-конструктор, которую JavaScript использует для создания всех таких объектов, устанавливая [[Prototype]] вновь созданного объекта для ссылки на объект-прототип из Object, Object.prototype.

Кроме того, все объекты, созданные с использованием синтаксиса объектного литерала, будут наследовать свойство constructor от объекта-прототипа, а не хранить свою собственную копию (см. строки 34–35) (если только мы не присвоим вручную значение свойству constructor нового объекта). Действительно, ручная установка значения любого свойства объекта создаст новую его копию в этом конкретном экземпляре объекта, затенив любое другое значение, хранящееся в объекте дальше в цепочке прототипов.

И наконец, обратите внимание, что только функции имеют объект-прототип, на который ссылается свойство prototype, поэтому obj не будет иметь свойства prototype по умолчанию (см. строку 39). Мы рассмотрим этот момент подробнее в следующем примере.

Пример 2

Давайте теперь повторим анализ, используя функциональный литерал func. Это довольно простая функция, которая просто записывает «hello world!» в консоль при вызове.

Как и в случае с литералом объекта obj в предыдущем примере, когда мы определили новый литерал функции func, JavaScript создаст его, используя конкретную функцию-конструктор, Function. Кроме того, только что созданный объект функции будет иметь свойство [[Prototype]], указывающее на Function.prototype, от которого func может наследовать методы и свойства, такие как call() (см. строку 21) и constructor (см. строку 15). Фрагмент кода в строке 10 подтверждает, что конкретный объект, на который ссылаются как на прототип func, действительно является Function.prototype.

Подобно тому, как функция-конструктор Object имеет свойство prototype, указывающее на объект-прототип, конструктор Function также имеет свой собственный объект-прототип, на который ссылается Function.prototype. В строке 11 мы дополнительно проверяем, что func был создан конструктором Function с использованием свойства constructor.

Основное различие между литералом объекта obj, изученным в предыдущем примере, и литералом функции func заключается в том, что теперь имеется новое свойство prototype, определенное для func.

Это означает, что func можно также использовать в качестве конструктора, вызвав его с помощью ключевого слова new (строка 26), и это создаст новый объект, например. newObj, [[Prototype]] которого будет указывать на func.prototype (см. строки 27–28).

Таким образом, все экземпляры объектов, созданные func, будут наследоваться от func.prototype. В частности, свойство constructor, к которому имеет доступ каждый экземпляр, будет указывать на исходную функцию func, служащую маркером того, как она была создана. Это может пригодиться позже, когда мы захотим исследовать, возможно, в целях отладки, происхождение или «тип» данного объекта, как показано в строках 29–30. Таким образом, в этом примере newObj является экземпляром типа func.

Конструктор Function является «особым» в том смысле, что он создает новые объекты типа «функция». Таким образом, объекты-функции, созданные конструктором Function, сами по себе являются конструкторами, которые могут создавать другие объекты. Действительно, его собственный объект-прототип, Function.protoype, сам по себе является функцией, что может быть подтверждено фрагментом кода typeof Function.prototype (см. строки 33–34), который должным образом возвращает «функцию».

Интересно, что конструктор Function сам является экземпляром своего собственного объекта-прототипа, т. е. Object.getPrototypeOf(Function) === Function.prototype возвращает true (см. строки 38–41). Даже конструктор Object, с которым мы столкнулись в предыдущем примере, и конструктор Array, который мы увидим в следующем примере, указывают на Function.prototype как на их [[Prototype]] (см. строки 46–47).

На самом деле, все функции в JavaScript, будь то обычные функции или конструкторы, будут «потомками» Function.prototype, т. е. они будут иметь Function.prototype в качестве прототипа где-то в своей цепочке прототипов, а Function.prototype.isPrototypeOf(<someFunction>) всегда будет возвращать true.

Пример 3

В следующем примере у нас есть литерал массива arr с двумя элементами, а также свойство length, необходимое для всех подобных массиву объектов. Как и в случае с литералом объекта и литералом функции, JavaScript использует встроенный конструктор для создания нового объекта массива за кулисами. Таким образом, [[Prototype]] из arr указывает на объект-прототип Array, Array.prototype, от которого он может наследовать методы экземпляра массива, такие как forEach(), map(), filter(), reduce(), join() и так далее.

Во фрагменте кода выше, в строке 27, мы дополнительно подтверждаем, что объект массива не имеет свойства prototype, указывающего на какой-либо объект-прототип. Только объекты-функции будут иметь это свойство по умолчанию, и оно используется для создания нового объекта, когда функция вызывается как конструктор с использованием ключевого слова new.

Наконец, как насчет свойства [[Prototype]] конструктора Array и свойства [[Prototype]] объекта-прототипа Array, Array.prototype? На какие объекты указывают эти два свойства? Это будет рассмотрено в следующем примере.

Пример 4

В следующем примере мы получаем полную картину цепочки прототипов литерала массива arr и его конструктора Array.

Объект arr наследуется от своего прототипа Array.prototype, который, в свою очередь, наследуется от своего прототипа Object.prototype. Мы можем проверить эту связь между объектами, пройдя по ссылкам в цепочке прототипов, т. е. проследив пунктирные линии от свойства [[Prototype]] объекта arr до Object.prototype. Это может быть подтверждено программно, а также показано в строках 7–8 фрагмента кода выше.

Мы также видим, что Object.prototype является последним объектом в цепочке прототипов, поскольку он не наследуется ни от каких других объектов, а его [[Prototype]] явно указывает на null (см. строку 14). Обратите внимание на различие между свойством, вычисляющим значение null, которое представляет преднамеренное отсутствие значения, и тем, что возвращает JavaScript при наличии несуществующего свойства, а именно undefined (см. строку 15).

Это проясняет всю цепочку прототипов arr. Как насчет конструктора Array? На что указывает его [[Prototype]]?

Во-первых, обратите внимание, что все три конструктора Array, Object и Function имеют тип «функция» (строки 20–22). Мы уже можем предположить, каким будет их объект-прототип, поскольку только конструктор Function может создавать объекты типа «функция» (см. строку 31). Действительно, код в строках 24–26 подтверждает, что все три конструктора ссылаются на Function.prototype как на свой прототип.

Более того, Function.prototype — это объект типа «функция», у которого есть прототип Object.prototype (см. строку 34). Эта довольно замкнутая и тесно связанная связь между конструкторами Object и Function показана на рис. 5 ниже.

Более подробное изучение этих двух конструкторов и объектов, создаваемых каждым из них, а именно литералов функций и литералов объектов, представлено в этой статье.