Когда мы создаем пустой объект в JavaScript, он выглядит пустым.

const geralt = {};

Мы не установили никаких свойств для этого объекта, поэтому это вызовет ошибку:

console.log( geralt.fight() );

Однако это не приведет к ошибке:

console.log( geralt.hasOwnProperty() );

Кажется, что метод hasOwnProperty() действительно существует, хотя мы его и не определили.

От куда это?

Тайные свойства объектов

Когда мы создаем объект, JavaScript автоматически добавляет к нему невидимое свойство. Это свойство __proto__.

Это довольно уродливое имя, поэтому его обычно произносят как prototype. Давайте проверим это:

console.log( geralt.__proto__ );
 // → {...}

Помните, что прототип — это просто произношение __proto__, а не фактическое имя, так что это не работает:

console.log( geralt.prototype );
 // → undefined

Объект.прототип

Мы установили, что JavaScript добавляет это секретное свойство к нашему объекту. Но что это на самом деле? Ну, __proto__ — это просто объект со своими свойствами по умолчанию.

Чтобы немного вернуться назад, Object — это функция-конструктор, которая ссылается на все объекты, созданные в документе. Мы используем Object.prototype для добавления свойств или методов ко всем объектам, которые наследуются от Object, а __proto__ на самом деле является ссылкой на этот стандартный прототип всех объектов.

Вот почему любой созданный вами пользовательский объект может получить доступ к файлу hasOwnProperty().

Если мы немного упростим, массивы и функции также являются объектами в JavaScript, и у них есть свои собственные прототипы по умолчанию, Array.prototype и Function.prototype соответственно.

console.log(Object.getPrototypeOf({}) === Object.prototype); 
 // → true
console.log(Object.getPrototypeOf(function () {}) === Function.prototype); 
 // → true
console.log(Object.getPrototypeOf([]) === Array.prototype); 
 // → true

Свойства прототипа

А вот и самое интересное. Когда мы пытаемся получить доступ к свойству, которого нет у нашего объекта, вместо этого ищется прототип объекта, чтобы проверить, существует ли такое свойство хотя бы там.

Вернемся к первому примеру, когда мы вызвали hasOwnProperty() для нашего пустого объекта geralt, вот что произошло:

  • браузер видит, что geralt.hasOwnProperty() не существует
  • так это выглядит в geralt.__proto__
  • обнаруживает, что geralt.__proto__.hasOwnProperty() существует, и вызывает его

Прототип полон сюрпризов. Например, помимо hasOwnProperty() у него есть еще несколько свойств. Например:

  • isPrototypeOf() — можем проверить родителя прототипа.
  • propertyIsEnumerable() — мы можем проверить, можно ли запустить цикл на свойстве.
  • toLocaleString() — мы можем форматировать даты.
  • toString() — мы можем представить объект простой строкой.
  • __proto__ — ПОДОЖДИТЕ ЧТО?

Да, действительно, в __proto__ есть еще один объект __proto__.

Сеть прототипов

Поскольку прототип — это объект (а у каждого объекта есть прототип), у нашего прототипа тоже есть свой прототип. Это называется цепочкой прототипов. Цепочка заканчивается, когда мы достигаем прототипа, который имеет нуль для своего собственного прототипа:

geralt.__proto__.__proto__
 // → null

Это конец цепочки, в которой:

  • геральт наш пользовательский объект
  • первый __proto__ является ссылкой на Object.prototype и наследует все его свойства
  • второй __proto__ равен null и, таким образом, завершает цепочку

Мы можем создавать объекты с пользовательскими прототипами. Другими словами, мы можем добавить ссылки в цепочку.

Сначала мы создаем вспомогательный объект:

const schoolOfTheWolf = {
  rest: function(witcherName) {
    console.log(`The witcher ${witcherName} rests at Kaer Morhen.`)
  }
}

Теперь мы можем использовать этот вспомогательный объект в качестве прототипа для нашего основного объекта со специальным ключевым словом Object.create, например так:

const witcher = Object.create(schoolOfTheWolf);
witcher.rest("Geralt");
// → The witcher Geralt rests at Kaer Morhen.

Теперь цепочка прототипов будет выглядеть так:

witcher.__proto__.__proto__.__proto__

В этой расширенной цепочке:

  • ведьмак наш пользовательский объект
  • первый __proto__ является ссылкой на schoolOfTheWolf и наследует все его свойства
  • второй __proto__ является ссылкой на Object.prototype и наследует все его свойства
  • третий __proto__ равен null и, таким образом, завершает цепочку

Теперь вы понимаете, почему следующие две строки кода работают и по сути одинаковы:

witcher.rest("Geralt");
// → The witcher Geralt rests at Kaer Morhen.
witcher.__proto__.rest("Geralt")
// → The witcher Geralt rests at Kaer Morhen.

И почему мы можем вызвать hasOwnProperty("rest") на первом прототипе и получить положительный результат.

console.log(witcher.__proto__.hasOwnProperty("rest"))
// → true

Заключение

Мы узнали о секретных свойствах объектов, исходящих из прототипов. Мы увидели цепочку прототипов и добавили в нее свою ссылку.

Если эта статья оказалась для вас полезной, нажмите кнопку хлопать в ладоши 👏. Кроме того, не стесняйтесь комментировать! Буду рад помочь :)