В программировании нам часто хочется взять что-то и расширить это.
Например, у нас есть объект user
с его свойствами и методами, и мы хотим сделать admin
и guest
слегка измененными его вариантами. Мы хотели бы повторно использовать то, что у нас есть в user
, а не копировать/переопределять его методы, а просто построить новый объект поверх него.
Прототипическое наследование — это особенность языка, которая помогает в этом.
В JavaScript объекты имеют специальное скрытое свойство [[Prototype]]
(как указано в спецификации), которое либо имеет null
, либо ссылается на другой объект. Этот объект называется «прототипом»:
Когда мы читаем свойство из object
, а оно отсутствует, JavaScript автоматически берёт его из прототипа. В программировании это называется «прототипическое наследование». И вскоре мы изучим множество примеров такого наследования, а также более интересные возможности языка, основанные на нем.
Свойство [[Prototype]]
является внутренним и скрытым, однако существует множество способов его установки.
Один из них — использовать специальное имя __proto__
, например:
пусть животное = { ест: правда }; пусть кролик = { прыжки: правда }; кролик.__proto__ = животное; // устанавливаем кролика.[[Prototype]] = животное
Теперь, если мы прочитаем свойство из rabbit
, а оно отсутствует, JavaScript автоматически возьмет его из animal
.
Например:
пусть животное = { ест: правда }; пусть кролик = { прыжки: правда }; кролик.__proto__ = животное; // (*) // теперь мы можем найти оба свойства в Rabbit: оповещение(кролик.ест); // истинный (**) оповещение(кролик.прыжки); // истинный
Здесь строка (*)
устанавливает animal
в качестве прототипа rabbit
.
Затем, когда alert
пытается прочитать свойство rabbit.eats
(**)
, его нет в rabbit
, поэтому JavaScript следует ссылке [[Prototype]]
и находит его в animal
(смотрите снизу вверх):
Здесь можно сказать, что « animal
является прототипом rabbit
» или « rabbit
прототипически наследует от animal
».
Итак, если у animal
много полезных свойств и приемов, то они автоматически становятся доступными у rabbit
. Такие свойства называются «наследственными».
Если у нас есть метод в animal
, его можно вызвать в rabbit
:
пусть животное = { ест: правда, ходить() { alert("Прогулка животного"); } }; пусть кролик = { прыжки: правда, __прото__: животное }; // прогулка взята из прототипа кролик.прогулка(); // Прогулка животного
Метод автоматически берётся из прототипа, вот так:
Цепочка прототипов может быть длиннее:
пусть животное = { ест: правда, ходить() { alert("Прогулка животного"); } }; пусть кролик = { прыжки: правда, __прото__: животное }; пусть longEar = { Длина ушей: 10, __прото__: кролик }; // прогулка взята из цепочки прототипов longEar.walk(); // Прогулка животного оповещение(longEar.jumps); // правда (от кролика)
Теперь, если мы прочитаем что-то из longEar
, а оно отсутствует, JavaScript будет искать это в rabbit
, а затем в animal
.
Есть только два ограничения:
Ссылки не могут идти по кругу. JavaScript выдаст ошибку, если мы попытаемся назначить __proto__
в круге.
Значение __proto__
может быть либо объектом, либо null
. Остальные типы игнорируются.
Тоже может быть очевидно, но всё же: [[Prototype]]
может быть только один. Объект не может наследовать от двух других.
__proto__
— это исторический метод получения/установки для [[Prototype]]
Распространенная ошибка начинающих разработчиков – не знать разницы между этими двумя понятиями.
Обратите внимание, что __proto__
— это не то же самое , что внутреннее свойство [[Prototype]]
. Это геттер/сеттер для [[Prototype]]
. Позже мы увидим ситуации, когда это имеет значение, а сейчас давайте просто будем иметь это в виду, пока мы углубляем наше понимание языка JavaScript.
Свойство __proto__
немного устарело. Он существует по историческим причинам: современный JavaScript предполагает, что мы должны использовать функции Object.getPrototypeOf/Object.setPrototypeOf
вместо того, чтобы получать/устанавливать прототип. Мы также рассмотрим эти функции позже.
По спецификации __proto__
должен поддерживаться только браузерами. Однако на самом деле все среды, включая серверную, поддерживают __proto__
, поэтому мы вполне можем его использовать.
Поскольку обозначение __proto__
интуитивно более очевидно, мы используем его в примерах.
Прототип используется только для чтения свойств.
Операции записи/удаления работают непосредственно с объектом.
В приведенном ниже примере мы назначаем rabbit
собственный метод walk
:
пусть животное = { ест: правда, ходить() { /* этот метод не будет использоваться кроликом */ } }; пусть кролик = { __прото__: животное }; кролик.walk = функция() { alert("Кролик! Подпрыгни-подпрыгни!"); }; кролик.прогулка(); // Кролик! Подпрыги-подпрыгни!
С этого момента вызов rabbit.walk()
находит метод непосредственно в объекте и выполняет его, не используя прототип:
Свойства аксессора являются исключением, поскольку назначение обрабатывается функцией установки. Таким образом, запись в такое свойство фактически аналогична вызову функции.
По этой причине admin.fullName
работает правильно в приведенном ниже коде:
пусть пользователь = { имя: «Джон», фамилия: «Смит», установить полное имя (значение) { [это.имя, эта.фамилия] = значение.split(" "); }, получить полное имя() { return `${this.name} ${this.surname}`; } }; пусть администратор = { __proto__: пользователь, isAdmin: правда }; оповещение(admin.fullName); // Джон Смит (*) // триггеры установки! admin.fullName = "Элис Купер"; // (**) оповещение(admin.fullName); // Элис Купер, изменено состояние администратора оповещение(user.fullName); // Джон Смит, состояние пользователя защищено
Здесь в строке (*)
свойство admin.fullName
имеет геттер в прототипе user
, поэтому оно и вызывается. А в строке (**)
свойство имеет установщик в прототипе, так оно и называется.
В приведенном выше примере может возникнуть интересный вопрос: каково значение this
внутреннего set fullName(value)
? Куда прописываются свойства this.name
и this.surname
: в user
или admin
?
Ответ прост: прототипы на this
никак не влияют.
Неважно, где находится метод: в объекте или его прототипе. При вызове метода this
всегда объект перед точкой.
Итак, вызов установщика admin.fullName=
использует в качестве this
admin
, а не user
.
На самом деле это очень важная вещь, потому что у нас может быть большой объект со множеством методов и объекты, наследующие от него. И когда наследующие объекты запускают унаследованные методы, они изменяют только свои собственные состояния, а не состояние большого объекта.
Например, здесь animal
представляет собой «хранилище методов», а rabbit
им пользуется.
Вызов rabbit.sleep()
устанавливает this.isSleeping
для объекта rabbit
:
// у животного есть методы пусть животное = { ходить() { если (!this.isSleeping) { alert(`Я иду`); } }, спать() { this.isSleeping = правда; } }; пусть кролик = { название: «Белый Кролик», __прото__: животное }; // изменяет Rabbit.isSleeping кролик.сон(); оповещение(rabbit.isSleeping); // истинный оповещение(animal.isSleeping); // неопределенно (в прототипе такого свойства нет)
Получившаяся картинка:
Если бы у нас были другие объекты, такие как bird
, snake
и т. д., наследующие от animal
, они также получили бы доступ к методам animal
. Но при каждом вызове метода this
будет соответствующий объект, оцениваемый во время вызова (до точки), а не animal
. Поэтому, когда мы записываем данные в this
, они сохраняются в этих объектах.
В результате методы являются общими, а состояние объекта — нет.
Цикл for..in
также перебирает унаследованные свойства.
Например:
пусть животное = { ест: правда }; пусть кролик = { прыжки: правда, __прото__: животное }; // Object.keys возвращает только собственные ключи оповещение(Object.keys(кролик)); // прыжки // for..in циклически перебирает как собственные, так и унаследованные ключи for(let prop in Rabbit) alert(prop); // прыгает, затем ест
Если это не то, что нам нужно, и мы хотели бы исключить унаследованные свойства, есть встроенный метод obj.hasOwnProperty(key): он возвращает true
если obj
имеет собственное (не унаследованное) свойство с именем key
.
Таким образом, мы можем отфильтровать унаследованные свойства (или сделать с ними что-то еще):
пусть животное = { ест: правда }; пусть кролик = { прыжки: правда, __прото__: животное }; for(let prop in Rabbit) { пусть isOwn = Rabbit.hasOwnProperty(prop); если (isOwn) { alert(`Наш: ${prop}`); // Наши: прыжки } еще { alert(`Унаследовано: ${prop}`); // Наследуется: ест } }
Здесь у нас есть следующая цепочка наследования: rabbit
наследует от animal
, который наследует от Object.prototype
(поскольку animal
— это буквальный объект {...}
, поэтому по умолчанию), а затем null
над ним:
Обратите внимание, есть одна забавная вещь. Откуда взялся метод rabbit.hasOwnProperty
? Мы не давали этому определения. Глядя на цепочку, мы видим, что метод предоставляется Object.prototype.hasOwnProperty
. Другими словами, это наследуется.
…Но почему hasOwnProperty
не появляется в цикле for..in
как это делают eats
и jumps
, если for..in
перечисляет унаследованные свойства?
Ответ прост: оно не перечислимо. Как и все другие свойства Object.prototype
, он имеет флаг enumerable:false
. А for..in
перечисляет только перечислимые свойства. Вот почему это и остальные свойства Object.prototype
не указаны.
Почти все другие методы получения ключа/значения игнорируют унаследованные свойства.
Почти все другие методы получения ключа/значения, такие как Object.keys
, Object.values
и т. д., игнорируют унаследованные свойства.
Они работают только с самим объектом. Свойства прототипа не учитываются.
В JavaScript все объекты имеют скрытое свойство [[Prototype]]
, которое является либо другим объектом, либо null
.
Мы можем использовать obj.__proto__
для доступа к нему (исторический метод получения/установки, есть и другие способы, о которых мы скоро расскажем).
Объект, на который ссылается [[Prototype]]
называется «прототипом».
Если мы хотим прочитать свойство obj
или вызвать метод, а он не существует, то JavaScript пытается найти его в прототипе.
Операции записи/удаления действуют непосредственно на объект, они не используют прототип (при условии, что это свойство данных, а не установщик).
Если мы вызываем obj.method()
и method
берется из прототипа, this
все равно ссылается на obj
. Таким образом, методы всегда работают с текущим объектом, даже если они унаследованы.
Цикл for..in
перебирает как свои собственные, так и унаследованные свойства. Все остальные методы получения ключа/значения работают только с самим объектом.
важность: 5
Вот код, который создает пару объектов, а затем изменяет их.
Какие значения отображаются в процессе?
пусть животное = { прыжки: ноль }; пусть кролик = { __proto__: животное, прыжки: правда }; оповещение(кролик.прыжки); // ? (1) удалить Rabbit.Jumps; оповещение(кролик.прыжки); // ? (2) удалить Animal.Jumps; оповещение(кролик.прыжки); // ? (3)
Должно быть 3 ответа.
true
, взято у rabbit
.
null
, взято у animal
.
undefined
, такого свойства больше нет.
важность: 5
Задача состоит из двух частей.
Учитывая следующие объекты:
пусть голова = { очки: 1 }; пусть таблица = { ручка: 3 }; пусть кровать = { лист: 1, подушка: 2 }; пусть карманы = { деньги: 2000 };
Используйте __proto__
для назначения прототипов таким образом, чтобы любой поиск свойств выполнялся по пути: pockets
→ bed
→ table
→ head
. Например, для pockets.pen
должно быть 3
(найдено в table
), а bed.glasses
должно быть 1
(найдено в head
).
Ответьте на вопрос: быстрее ли получить glasses
в формате pockets.glasses
или head.glasses
? При необходимости сделайте сравнительный анализ.
Давайте добавим __proto__
:
пусть голова = { очки: 1 }; пусть таблица = { ручка: 3, __прото__: голова }; пусть кровать = { лист: 1, подушка: 2, __proto__: таблица }; пусть карманы = { деньги: 2000, __proto__: кровать }; оповещение( Pockets.pen ); // 3 оповещение(кровать.очки); // 1 оповещение(таблица.деньги); // неопределенный
В современных движках с точки зрения производительности нет разницы, берём ли мы свойство у объекта или его прототипа. Они запоминают, где было найдено свойство, и повторно используют его в следующем запросе.
Например, для pockets.glasses
они запоминают, где нашли glasses
(в head
), и в следующий раз будут искать именно там. Они также достаточно умны, чтобы обновлять внутренние кэши, если что-то меняется, поэтому оптимизация безопасна.
важность: 5
У нас есть rabbit
наследующий от animal
.
Если мы вызовем rabbit.eat()
, какой объект получит full
свойство: animal
или rabbit
?
пусть животное = { есть() { this.full = правда; } }; пусть кролик = { __прото__: животное }; кролик.есть();
Ответ: rabbit
.
Это потому, this
перед точкой стоит объект, поэтому rabbit.eat()
модифицирует rabbit
.
Поиск свойств и выполнение — это две разные вещи.
Метод rabbit.eat
сначала встречается в прототипе, а затем выполняется с помощью this=rabbit
.
важность: 5
У нас есть два хомяка: speedy
и lazy
наследующиеся от общего объекта hamster
.
Когда мы кормим одного из них, другой тоже насыщается. Почему? Как мы можем это исправить?
пусть хомяк = { желудок: [], есть (еду) { this.stomach.push(еда); } }; пусть спиди = { __proto__: хомяк }; пусть ленивый = { __proto__: хомяк }; // Этот нашел еду Speedy.eat("яблоко"); оповещение(скоро.желудок); // яблоко // У этого тоже есть, почему? исправьте, пожалуйста. оповещение(ленивый.желудок); // яблоко
Давайте внимательно посмотрим, что происходит при вызове speedy.eat("apple")
.
Метод speedy.eat
находится в прототипе ( =hamster
), затем выполняется с помощью this=speedy
(объект перед точкой).
Затем this.stomach.push()
нужно найти свойство stomach
и вызвать для него push
. Он ищет this
stomach
( =speedy
), но ничего не находит.
Затем он следует по цепочке прототипов и находит stomach
hamster
.
Затем он вызывает на него push
, добавляя пищу в желудок прототипа .
Итак, у всех хомяков один желудок!
Как для lazy.stomach.push(...)
так и speedy.stomach.push()
свойство stomach
находится в прототипе (поскольку его нет в самом объекте), а затем в него помещаются новые данные.
Обратите внимание, что такого не происходит в случае простого присваивания this.stomach=
:
пусть хомяк = { желудок: [], есть (еда) { // присваиваем this.stomach вместо this.stomach.push this.stomach = [еда]; } }; пусть спиди = { __proto__: хомяк }; пусть ленивый = { __proto__: хомяк }; // Быстрее нашел еду Speedy.eat("яблоко"); оповещение(скоро.желудок); // яблоко // У ленивого желудок пуст оповещение(ленивый.желудок); // <ничего>
Теперь все работает нормально, потому что this.stomach=
не выполняет поиск stomach
. Значение записывается непосредственно в this
объект.
Также мы можем полностью избежать этой проблемы, позаботившись о том, чтобы у каждого хомячка был свой желудок:
пусть хомяк = { желудок: [], есть (еду) { this.stomach.push(еда); } }; пусть спиди = { __proto__: хомяк, желудок: [] }; пусть ленивый = { __proto__: хомяк, желудок: [] }; // Скороспелый нашел еду Speedy.eat("яблоко"); оповещение(скоро.желудок); // яблоко // У ленивого желудок пуст оповещение(ленивый.желудок); // <ничего>
В качестве общего решения все свойства, описывающие состояние конкретного объекта, например stomach
выше, должны быть записаны в этот объект. Это предотвращает подобные проблемы.