В программировании мы часто хотим взять что-то и расширить.
Например, у нас есть объект 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]]. Объект не может наследоваться от двух других объектов.