Прототип похож на отношения. Объект может указывать на другой объект как на свой прототип.
Рассмотрим следующий пример:
let human = { hasRedHat: true, }; let mario = { name: "Mario", }; console.log(mario.hasRedHat); // undefined
Переменная mario
указывает на объект, у которого нет свойства hasRedHat
. Регистрация этого свойства вызовет поведение JavaScript по умолчанию и вернет undefined
.
Однако вы можете указать JavaScript продолжать искать отсутствующее свойство в другом объекте. Это делается путем указания специального свойства __proto__
, также известного как прототип объекта.
Фактически любой объект JavaScript может выбрать в качестве прототипа другой объект:
let human = { hasRedHat: true, }; let mario = { name: "Mario", __proto__: human, // Look for other properties in the "human" object }; console.log(mario.hasRedHat); // true
В приведенном выше примере вы начинаете с просмотра провода mario
, ведущего к объекту. Сначала вы посмотрите, есть ли у этого объекта свойство hasRedHat
. Ответ отрицательный, но у него есть прототип. Затем вы следуете по проводу __proto__
, который ведет к другому объекту (human
). Вы смотрите на этот объект, чтобы увидеть, имеет ли он свойство hasRedHat
. Ответ положительный, поэтому вы следуете по проводу hasRedHat
, ведущему к true
. Следовательно, результатом mario.hasRedHat
является true
.
Обратите внимание, что это не означает, что mario
имеет свойство hasRedHat
. Его объект-прототип, тот самый, на который указывает human
. С помощью __proto__
вы указываете JavaScript запрашивать другой объект.
Цепочка прототипов
Думайте о прототипе как об отношении между объектами, в котором один объект указывает на другой объект как на свой прототип. Эта последовательность объектов называется цепочкой прототипов объекта и не может быть замкнутой.
let mammal = { hasBrain: true, }; let human = { __proto__: mammal, hasRedHat: true, }; let mario = { __proto__: human, name: "Mario", }; console.log(mario.hasBrain); // true
Вы можете видеть, что JavaScript будет искать свойство сначала в объекте mario
, затем в его прототипе, затем в прототипе этого объекта и так далее. Вы получите undefined
только в том случае, если у вас закончились прототипы и вы еще не нашли то свойство, которое искали.
Затенение
Рассмотрим следующий пример, в котором оба объекта имеют свойство с именем hasRedHat
:
Рассмотрим следующий пример, в котором оба объекта имеют свойство с именем hasRedHat
:
let human = { hasRedHat: false, }; let mario = { __proto__: human, hasRedHat: true, // This object has its own "hasRedHat" property }; console.log(human.hasRedHat); // false console.log(mario.hasRedHat); // true
Если бы у mario
не было собственного свойства hasRedHat
, вам пришлось бы смотреть на прототип. Но поскольку объект, на который указывает mario
, имеет собственное свойство hasRedHat
, вам не нужно искать ответ. Как только вы найдете недвижимость, вы остановите поиск.
Если вы хотите проверить, есть ли у объекта собственный провод свойств с определенным именем, вы можете вызвать встроенную функцию hasOwnProperty
. Он возвращает true
для собственных свойств и не смотрит на прототипы:
console.log(human.hasOwnProperty("hasRedHat")); // true console.log(mario.hasOwnProperty("hasRedHat")); // true
Назначение
Когда вы читаете свойство, не существующее в объекте, вы продолжаете искать его в цепочке прототипов. Если не найдете, получите undefined
.
Однако когда вы записываете свойство, не существующее в объекте, присваивание создает свойство для этого объекта. Поэтому прототипы в данном случае роли не играют.
Рассмотрим следующий пример:
let human = { hasRedHat: false, }; let mario = { __proto__: human, }; console.log(human.hasRedHat); // false console.log(mario.hasRedHat); // false mario.hasRedHat = true; console.log(human.hasRedHat); // false console.log(mario.hasRedHat); // true
Перед присваиванием оба выражения приводят к false
. Затем вы выполняете это назначение:
mario.hasRedHat = true;
Присваивания происходят на самом объекте. Таким образом, mario.hasRedHat = true
создает собственное новое свойство с именем hasRedHat
для объекта, на который указывает mario
. На прототип это не влияет. В результате human.hasRedHat
по-прежнему false
, а mario.hasRedHat
теперь true
.
Прототип объекта
Прототип объекта — это специальный объект, который используется в качестве прототипа по умолчанию для всех объектов.
let obj = {}; console.log(obj);
Хотя вы создали то, что кажется пустым объектом, obj.__proto__
не является null
или undefined
. Он имеет скрытый провод __proto__
, который по умолчанию указывает на Prototype объекта. Это позволяет вам получить доступ к встроенным методам объекта:
let mario = { name: "Mario", }; console.log(mario.hasOwnProperty); // (function) console.log(mario.toString); // (function)
Эти встроенные свойства являются обычными свойствами прототипа объекта, к которым вы можете получить доступ, поскольку прототипом объекта mario
является прототип объекта.
Прототип объекта — это объект без прототипа. Вам не нужно будет создавать подобные объекты, но полезно знать, что он существует и создается путем настройки свойства __proto__
.
let nintendo = { __proto__: null, }; console.log(nintendo.hasOwnProperty); // undefined console.log(nintendo.toString); // undefined
В приведенном выше примере будет создан объект, не имеющий прототипа и, как следствие, даже не имеющий встроенных методов объекта.
Загрязнение прототипа
JavaScript может искать недостающие свойства в прототипе. Однако, поскольку большинство объектов имеют один и тот же прототип, это также может привести к тому, что новые свойства появятся у всех объектов, изменяя прототип.
Мутация общего прототипа называется загрязнением прототипа:
let obj = {}; obj.__proto__.vehicle = "Standard Kart"; let mario = { name: "Mario", }; let luigi = { name: "Luigi", }; console.log(mario.vehicle); // "Standard Kart" console.log(luigi.vehicle); // "Standard Kart"
Вы изменили прототип объекта, добавив к нему свойство vehicle
. В результате любой объект будет использовать один и тот же vehicle
.
В прошлом. Загрязнение прототипов было популярным методом добавления новых функций в JavaScript. Однако веб-сообщество больше не рекомендует это.
И еще: __proto__
против prototype
Просматривая документацию MDN, вы столкнетесь со свойством prototype
. До того, как в JavaScript была добавлена поддержка классов, их обычно писали как функции, возвращающие объекты. Всякий раз, когда вы хотели поделиться прототипом с некоторыми общими методами, вам приходилось вручную добавлять __proto__
к каждому объекту.
function SuperMarioMushroom() { return { type: "power-up" }; } let mushroomProto = { consume() { console.log("I got the power!"); }, }; let mushroom1 = SuperMarioMushroom(); mushroom1.__proto__ = mushroomProto; mushroom1.consume(); let mushroom2 = SuperMarioMushroom(); mushroom2.__proto__ = mushroomProto; mushroom2.consume();
Чтобы решить эту проблему, в JavaScript было добавлено ключевое слово new
. Если вы используете ключевое слово new
перед вызовом функции, объект создается автоматически, без необходимости возвращать его из функции, и становится доступным как this
.
Кроме того, __proto__
объекта будет установлено на то, что вы добавите в свойство prototype
функции, без необходимости устанавливать его вручную.
function SuperMarioMushroom() { this.type = "power-up"; } // prototype approach 1 SuperMarioMushroom.prototype = { consume() { console.log("I got the power!"); }, }; // prototype approach 2 SuperMarioMushroom.prototype.consume = function () { console.log("I got the power!"); }; let mushroom1 = new SuperMarioMushroom(); // __proto__: SuperMarioMushroom.prototype mushroom1.consume(); let mushroom2 = new SuperMarioMushroom(); // __proto__: SuperMarioMushroom.prototype mushroom2.consume();
В приведенном выше примере показано, как свойство prototype
функции позволяет настроить __proto__
объектов, которые вы получаете с помощью вызовов new
.
Хотя свойство prototype
по-прежнему доступно во встроенных функциях, в современном JavaScript вместо этого вы обычно используете синтаксис class
с конструктором:
class SuperMarioMushroom { constructor() { this.type = "power-up"; } consume() { console.log("I got the power!"); } } let mushroom1 = new SuperMarioMushroom(); // __proto__: SuperMarioMushroom.prototype mushroom1.consume(); let mushroom2 = new SuperMarioMushroom(); // __proto__: SuperMarioMushroom.prototype mushroom2.consume();
Несмотря на то, что использование синтаксиса __proto__
не рекомендуется, помните, что под капотом mushroom1.consume()
по-прежнему находит свойство consume
, ища его через __proto__
.
Использование прототипов для создания модели наследования классов стало настолько распространенным, что JavaScript добавил синтаксис class
в качестве соглашения, которое скрывает как __proto__
, так и prototype
.