В программировании мы часто хотим взять что-то и расширить.
Например, у нас есть объект user
со своими свойствами и методами, и мы хотим создать объекты admin
и guest
как его слегка изменённые варианты. Мы хотели бы повторно использовать то, что есть у объекта user
, не копировать/переопределять его методы, а просто создать новый объект на его основе.
Прототипное наследование — это возможность языка, которая помогает в этом.
[[Prototype]]
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]]
(так оно названо в спецификации), которое либо равно null
, либо ссылается на другой объект. Этот объект называется «прототип»:
Прототип даёт нам немного «магии». Когда мы хотим прочитать свойство из object
, а оно отсутствует, JavaScript автоматически берёт его из прототипа. В программировании такой механизм называется «прототипным наследованием». Многие интересные возможности языка и техники программирования основываются на нём.
Свойство [[Prototype]]
является внутренним и скрытым, но есть много способов задать его.
Одним из них является использование __proto__
, например так:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal;
Свойство __proto__
— исторически обусловленный геттер/сеттер для [[Prototype]]
Обратите внимание, что __proto__
— не то же самое, что [[Prototype]]
. Это геттер/сеттер для него.
Он существует по историческим причинам, в современном языке его заменяют функции Object.getPrototypeOf/Object.setPrototypeOf
, которые также получают/устанавливают прототип. Мы рассмотрим причины этого и сами функции позже.
По спецификации __proto__
должен поддерживаться только браузерами, но по факту все среды, включая серверную, поддерживают его. Далее мы будем в примерах использовать __proto__
, так как это самый короткий и интуитивно понятный способ установки и чтения прототипа.
Если мы ищем свойство в rabbit
, а оно отсутствует, JavaScript автоматически берёт его из animal
.
Например:
let animal = {
eats: true
};
let rabbit = {
jumps: true
};
rabbit.__proto__ = animal; // (*)
// теперь мы можем найти оба свойства в rabbit:
alert( rabbit.eats ); // true (**)
alert( rabbit.jumps ); // true
Здесь строка (*)
устанавливает animal
как прототип для rabbit
.
Затем, когда alert
пытается прочитать свойство rabbit.eats
(**)
, его нет в rabbit
, поэтому JavaScript следует по ссылке [[Prototype]]
и находит его в animal
(смотрите снизу вверх):
Здесь мы можем сказать, что “animal
является прототипом rabbit
” или “rabbit
прототипно наследует от animal
“.
Так что если у animal
много полезных свойств и методов, то они автоматически становятся доступными у rabbit
. Такие свойства называются «унаследованными».
Если у нас есть метод в animal
, он может быть вызван на rabbit
:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
// walk взят из прототипа
rabbit.walk(); // Animal walk
Метод автоматически берётся из прототипа:
Цепочка прототипов может быть длиннее:
let animal = {
eats: true,
walk() {
alert("Animal walk");
}
};
let rabbit = {
jumps: true,
__proto__: animal
};
let longEar = {
earLength: 10,
__proto__: rabbit
};
// walk взят из цепочки прототипов
longEar.walk(); // Animal walk
alert(longEar.jumps); // true (из rabbit)
Есть только два ограничения:
- Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить
__proto__
по кругу. - Значение
__proto__
может быть объектом илиnull
. Другие типы игнорируются.
Это вполне очевидно, но всё же: может быть только один [[Prototype]]
. Объект не может наследоваться от двух других объектов.