Эта статья является первой в обширной серии из пяти частей о прототипном наследовании.

Часть 1 — Понимание цепочки прототипов
Часть 2 — Наследование прототипов с помощью Object.create
Часть 3 — Наследование прототипов с функциями конструктора
Часть 4 — Скоро!
Часть 5 — Скоро!

Когда я перешел с Java и C++ на JavaScript, я нашел концепцию прототипного наследования чрезвычайно запутанной. Я просто не мог уложиться в голове. Это слишком отличалось от того, к чему я привык.

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

Чтобы убедиться, что вы полностью усвоили все концепции, представленные в этой серии, я привожу упражнения (и решения) в конце каждой части. Я настоятельно рекомендую пройти каждое упражнение.

Смело обращайтесь с вопросами! Буду рад вашим отзывам и любой конструктивной критике.

Часть 1. Понимание цепочки прототипов

В JavaScript каждый объект имеет специальное соединение, называемое [[prototype]], которое связывает его с другим объектом, известным как его прототип. Этот объект-прототип может иметь свой собственный прототип, образуя цепочку. Цепочка продолжается до тех пор, пока не достигнет объекта с нулевым прототипом.

Чтобы визуализировать это, представьте генеалогическое древо, где у каждого человека есть родитель, у которого, в свою очередь, есть свой родитель и так далее. Точно так же объекты в JavaScript имеют прототип, и этот прототип также может иметь свой собственный прототип.

Вот пример цепочки прототипов:

Большинство объектов JavaScript имеют прототип Object.prototype. Однако есть определенные объекты, которые не наследуются напрямую от Object.prototype. Например:

  1. Объекты, созданные с помощью Object.create(someProto), имеют прототип someProto. Я подробно расскажу о Object.create во Части 2 этой серии.
  2. Некоторые встроенные объекты в JavaScript, такие как Array, Function, RegExp и Date, имеют собственные отличные прототипы (Array.prototype, Function.prototype, RegExp.prototype и Date.prototype соответственно). Хотя прототипы этих встроенных объектов могут в конечном итоге ссылаться на Object.prototype, сами объекты не имеют прямого прототипа Object.prototype.

При попытке доступа к свойству объекта поиск этого свойства выходит за пределы самого объекта. Он продолжается до прототипа объекта, прототипа прототипа и так далее, пока либо не будет обнаружено свойство с совпадающим именем, либо не будет достигнут конец цепочки прототипов. Если при поиске не удается найти нужное свойство, результатом операции будет undefined для свойства-значения или TypeError для свойства с функциональным значением.

Я проиллюстрирую изложенную выше концепцию, рассмотрев два примера:

Пример 1

const pumba = { 
  name: 'Pumba',
  age: 15
}
console.log(pumba.toString()) // [object object]

Когда мы объявляем объектный литерал, такой как pumba во фрагменте выше, он автоматически получает Object.prototype в качестве своего прототипа. По сути, наш небольшой фрагмент выше создал следующую цепочку прототипов:

Обратите внимание, что мы никогда не определяли toString для pumba, так откуда же это взялось? Когда мы попытались получить доступ к toString, и он не был найден на pumba, среда выполнения JavaScript проверила прототип pumba, которым оказался Object.prototype. Этот объект действительно имеет метод toString.

На самом деле, если мы console.log(Object.prototype) в консоли Chrome, мы видим метод toString вместе с другими методами, с которыми вы, возможно, знакомы.

Пример 2

const arr = ['Pokity', 'Wokity', 'Lokity'];
console.log(arr.at(1)); // Wokity

Как наш массив arr, получил метод at? Как упоминалось выше, ссылка [[prototype]] массива указывает на Array.prototype. То есть приведенный выше код создает следующую цепочку прототипов:

Если мы рассмотрим объект Array.prototype в консоли Chrome, мы увидим

Array.prototype имеет метод at.

Теперь, когда мы понимаем цепочку прототипов, возникает следующий естественный вопрос: как нам получить доступ к прототипу объекта obj? Есть два основных способа:

  1. Object.getPrototypeOf(obj)
  2. obj.__proto__
const newObj = {
    key: 'value'
}
console.log(newObj.__proto__ === Object.prototype); // true
console.log(Object.getPrototypeOf(newObj) === Object.prototype); // true

Мы будем придерживаться Object.getPrototypeOf, так как использование __proto__ больше не рекомендуется. Подробнее о том, почему, смотрите здесь.

К этому моменту у вас должно быть хорошее представление о том, как работает цепочка прототипов. Давайте продолжим и обсудим три основных способа создания цепочки прототипов:

  1. Объект.создать
  2. Функции конструктора
  3. Конструкторы синтаксиса класса

Перейдите к Часть 2, Наследование прототипов с помощью Object.create.

Упражнения

Упражнение 1

const myBirthday = new Date('December 11 1986'); 
console.log(myBirthday.getFullYear()); // 1986

Мы никогда не определяли функцию getFullYear для myBirthday.

На самом деле оператор myBirthday.hasOwnProperty(‘getFullYear’) оценивается как false.

Почему мы не получаем TypeError при запуске приведенного выше кода?

Решение

Объекты Date имеют прототип Date.prototype. Таким образом, мы имеем следующую цепочку прототипов:

null ← Object.prototype ← Date.prototype ← myBirthday

Вы можете убедиться в этом, выполнив следующий код

console.log(Object.getPrototypeOf(myBirthday) === Date.prototype);
console.log(Object.getPrototypeOf(Date.prototype) === Object.prototype);
console.log(Object.getPrototypeOf(Object.prototype) === null);

Date.prototype имеет метод getFullYear. Оператор Date.prototype.hasOwnProperty(‘getFullYear’) оценивается как true.

Таким образом, когда среда выполнения JavaScript не могла найти метод getFullYear для объекта myBirthday, она проверяла его прототип Date.prototype. У этого объекта действительно есть метод getFullYear, поэтому он его выполнил.

Полное раскрытие информации: в душе я разработчик. Я предпочитаю писать такие вещи, как dog.isWalking, а не «собака идет». Учитывая, что я занимаюсь этим годами, мои навыки английской грамматики медленно, но верно ухудшаются. Когда я работал над этой серией, я заметил, что временами то, что я писал, звучало как тарабарщина. Итак, я опирался на ChatGPT. Я также попытался сгенерировать изображения для этой статьи, используя DALL·E 2; к сожалению, все они были… странными.