- Наследование: ссылка
__proto__ - Прототип и
this - Цепочка прототипов
- Установка и чтение прототипа в ES5
- Кросс-браузерное наследование
- Итого
Наследование — это способ расширения функциональности существующего объекта, без его изменения.
Для этого создаётся новый объект («потомок») и указывается, что он «наследует» от существующего («родителя»).
В дальнейшем, при обращении к свойству потомка, интерпретатор сначала проверяет, есть ли в нём это свойство, и если нет — ищет в родителе. Таким образом получается, что потомок имеет доступ ко всем свойствам родителя, но может добавить к ним свои.
Наследование: ссылка __proto__
Теперь перейдём к конкретике и примерам.
Наследование в JavaScript реализуется при помощи специального свойства __proto__.
Для того, чтобы один объект rabbit, наследовал от другого animal, должно быть rabbit.__proto__ = animal.

Работает это так:
Когда запрашивается свойство rabbit, интерпретатор ищет его сначала в самом объекте rabbit, а если не находит — в объекте rabbit.__proto__, то есть, в данном случае, в animal.
Ниже идет пример с __proto__. Он работает только в браузерах Chrome и Firefox. Это для простоты, позже мы сделаем всё кросс-браузерно.
var animal = { eats: true };
var rabbit = { jumps: true };
*!*
rabbit.__proto__ = animal; // унаследовать
*/!*
alert(rabbit.eats); // true
Значение свойства rabbit.eats в действительности получено из animal.
Иллюстрация происходящего (поиск идет снизу вверх):

Объект, на который ссылается rabbit.__proto__, называется прототипом rabbit.
В нашем случае прототипом для rabbit является animal.
Прототип используется для поиска только если свойство не найдено в самом объекте.
Например, в следующем примере eats берется из самого объекта, до animal дело не доходит:
var animal = { eats: true };
var fedUpRabbit = { eats: false };
fedUpRabbit.__proto__ = animal;
*!*
alert(fedUpRabbit.eats); // false, свойство взято из fedUpRabbit
*/!*

Когда значение нужно найти, например при чтении свойства alert(this.prop) — интерпретатор ищет его сначала в объекте, затем в прототипе.
Когда значение нужно записать obj.prop = value или удалить delete obj.prop — запись идет напрямую в объект.
В англоязычной спецификации EcmaScript свойство __proto__ обозначено как [[Prototype]]. Двойные квадратные скобки здесь важны, чтобы не перепутать его с совсем другим свойством, которое называется prototype.
Прототип и this
Если коротко — прототип и this никак не связаны.
Например:
var animal = {
eat: function() {
alert("Я поело");
*!*
this.full = true; // (*)
*/!*
}
};
var rabbit = { /* ... */ };
rabbit.__proto__ = animal;
*!*
rabbit.eat();
*/!*
Какое значение this будет использовано при вызове (*)? animal или rabbit?
Чтобы это понять, посмотрим как выполняется rabbit.eat():
- Интерпретатор ищет
rabbit.eat, чтобы его вызвать. Но свойствоeatотсутствует в объектеrabbit, поэтому он идет по ссылкеrabbit.__proto__и находит это свойство там.
- Функция
eatзапускается. Контекст ставится в равным объекту перед точкой, т.е.this = rabbit.Итак — получается, что команда
this.full = trueустанавливает свойствоfullв самом объектеrabbit:
Что мы имеем? Вызывается метод родителя, но при этом в контексте самого объекта. И это удобно! Особенно если наследование многоуровневое..
Цепочка прототипов
У объекта, который является __proto__, может быть свой __proto__, у того — свой, и так далее.
При доступе к свойству, интерпретатор будет искать его по цепочке:
- В самом объекте,
- В его прототипе
__proto__, - В
__proto__его прототипа, - … и так далее:
obj.__proto__ -> obj.__proto__.__proto__ -> ...
Например, цепочка наследования из трех объектов donkey -> winnie -> owl:
var owl = {
sum: function(a, b) {
return a+b;
}
}
var winnie = { /* ... */ }
winnie.__proto__ = owl;
var donkey = { /* ... */ }
donkey.__proto__ = winnie;
alert( donkey.sum(2,2) ); // "4" ответит owl

Установка и чтение прототипа в ES5
Свойство __proto__ — нестандартное. Оно доступно в Firefox и Chrome.
В других современных браузерах, включая IE9+ есть стандартные методы для работы с прототипом (что делать в несовременных — мы разберём дальше).
- Object.create(proto)
- Создает пустой объект с прототипом
proto.Например:
var animal = { eats: true }; *!* rabbit = Object.create(animal); */!* alert(rabbit.eats); // trueЭтот код создал пустой объект
rabbitс прототипомanimal:
Мы можем добавить свойства в новый объект
rabbit:
var animal = { eats: true }; *!* rabbit = Object.create(animal); */!* rabbit.jumps = true;Станет:

У метода
Object.createсуществует и второй необязательный аргумент, который позволяет задать свойства нового объекта. Но он никак не относится к наследованию, поэтому здесь мы его пропустим.
- Object.getPrototypeOf(obj)
- Возвращает значение
obj.__proto__. Этот метод является стандартным и работает во всех браузерах, включая те, что не поддерживают свойство__proto__:var animal = { eats: true }; rabbit = Object.create(animal); *!* alert( Object.getPrototypeOf(rabbit) === animal ); // true */!*
Таким образом, современные браузеры позволяют создавать объект с заданным прототипом, читать значение прототипа, но не менять его.
Кросс-браузерное наследование
Свойство prototype
Функции-конструкторы в JavaScript также умеют создавать объект с заданным __proto__. Эта возможность JavaScript поддерживается во всех браузерах, включая IE6+.
Рассмотрим следующий пример:
var animal = { eats: true }
function Rabbit(name) {
this.name = name;
}
*!*
Rabbit.prototype = animal;
*/!*
var rabbit = new Rabbit('John');
alert( rabbit.eats ); // true, т.к. rabbit.__proto__ == animal
При создании объекта через new, в его прототип __proto__ копируется ссылка из prototype функции-конструктора
Код Rabbit.prototype = animal дословно означает следующее: «установить __proto__ = animal для всех объектов, создаваемых через new Rabbit».
Примитивное значение, такое как число или строка, будет проигнорировано.
Каков будет результат выполнения этого кода? Почему?
function Rabbit(name) {
this.name = name;
}
*!*
Rabbit.prototype = { eats: true };
var rabbit = new Rabbit("Кроль");
Rabbit.prototype = {};
*/!*
alert(rabbit.eats);
Результатом будет true, т.к. свойство prototype изменено после создания объекта через new Rabbit.
Это окажет влияние на новые объекты, а значением rabbit.__proto__ по-прежнему является animal.
Изменение свойства prototype для функции-конструктора не влияет на уже созданные ей объекты.
Каков будет результат выполнения этого кода? Почему?
function Rabbit(name) {
this.name = name;
}
var animal = { eats: true };
Rabbit.prototype = animal;
var rabbit = new Rabbit("Кроль");
*!*
Rabbit.prototype.eats = false;
*/!*
alert(rabbit.eats);
Результатом будет false, т.к. теперь мы меняем не prototype, а непосредственно идём внутрь объекта-прототипа.
Эти две строчки идентичны:
Rabbit.prototype.eats = false; animal.eats = false;
Ссылки rabbit.__proto__ и Rabbit.prototype ссылаются на один и тот же объект animal, поэтому изменения в нём будут видны.
Функция inherit для эмуляции Object.create(proto)
Встроенный метод Object.create(proto), который создаёт пустой объект с данным прототипом, к сожалению, не работает в IE<9.
Но его можно эмулировать способом, который работает во всех браузерах.
Кросс-браузерный аналог inherit выглядит следующим образом:
function inherit(proto) {
function F() {}
F.prototype = proto;
var object = new F;
return object;
}
Результат вызова inherit(animal) идентичен Object.create(animal): новый пустой объект со свойством object.__proto__ = animal.
Например:
var animal = { eats: true };
var rabbit = inherit(animal);
alert(rabbit.eats); // true
Посмотрим в подробностях, за счёт чего работает эта функция. В ней всего несколько строк:
function inherit(proto) {
function F() {} // (1)
F.prototype = proto // (2)
var object = new F; // (3)
return object; // (4)
}
- Создана новая функция
F. Она ничего не делает сthis, так что вызовnew Fвернёт пустой объект. - Свойство
F.prototypeустанавливается в будущий прототипproto - Результатом вызова
new Fбудет пустой объект с__proto__равным значениюF.prototype. - Готово! Мы получили пустой объект с заданным прототипом.
Эта функция широко используется в библиотеках и фреймворках.
Есть функция Menu, которая получает объект аргументов options:
/* options содержит настройки меню: width, height и т.п. */
function Menu(options) {
...
}
Ряд опций должны иметь значение по умолчанию. Мы могли бы проставить их напрямую в объекте options:
function Menu(options) {
options.width = options.width || 300; // по умолчанию ширина 300
...
}
… Но такие изменения могут привести к непредвиденным результатам, т.к. объект options может быть повторно использован во внешнем коде. Он передается в Menu для того, чтобы параметры из него читали, а не писали.
Один из способов обойти это — склонировать options путём копирования всех свойств из него в новый объект, который уже изменяется.
Еще один способ — скопировать все свойства в локальные переменные.
Как решить проблему без копирования, с использованием наследования?
Можно унаследовать от options и добавлять/менять опции в потомке:
function inherit(proto) {
function F() {}
F.prototype = proto;
return new F;
}
function Menu(options) {
var opts = inherit(options);
opts.width = opts.width || 300;
alert(opts.width); // возьмёт width из opts
alert(opts.height); // возьмёт height из options
...
}
Все изменения будут происходить в наследнике, а исходный объект останется незатронутым.
P.S. При этом нельзя удалять параметры. Вызов delete opts.height никак не повлияет на возможность получить opts.height, если это свойство находится в исходном объекте.
Метод hasOwnProperty
Метод obj.hasOwnProperty(prop) есть у всех объектов. Он позволяет проверить, принадлежит ли свойство prop самому объекту obj, без учета его прототипа.
Например:
function Rabbit(name) {
this.name = name;
}
Rabbit.prototype = { eats: true };
var rab = new Rabbit('Кроль');
*!*
alert(rab.hasOwnProperty('eats')); // false, свойство в прототипе
alert(rab.hasOwnProperty('name')); // true, свойство в объекте
*/!*
Цикл по свойствам с унаследованными и без них
Цикл for..in перебирает все свойства в объекте и его прототипе.
Например:
function Rabbit(name) {
this.name = name; // name записали в объект
}
Rabbit.prototype = { eats: true }; // eats будет в прототипе
var rabbit = new Rabbit('Кроль');
*!*
for (var p in rabbit) {
alert (p + " = " + rabbit[p]); // выводит и "name" и "eats"
}
*/!*
Чтобы получить список свойств, которые принадлежат самому объекту, а не его прототипу, отфильтруем их проверкой hasOwnProperty:
function Rabbit(name) {
this.name = name
}
Rabbit.prototype = { eats: true };
var rabbit = new Rabbit('John');
for (var p in rabbit) {
*!*
if (rabbit.hasOwnProperty(p)) {
*/!*
alert (p + " = " + rabbit[p]); // выведет только "name"
}
}
Чтобы уменьшить уровень вложенности, этот цикл лучше переписать так:
for (var p in rabbit) {
*!*
if (!rabbit.hasOwnProperty(p)) continue; // отфильтровать "eats"
*/!*
alert (p + " = " + rabbit[p]) // выведет только "name"
}
Итого
Наследование реализуется через специальное свойство __proto__ (в спецификации [[Prototype]]).
- При чтении свойства интерпретатор ищет его сначала в самом объекте, а потом следует по ссылке
__proto__и ищет там. - Операции записи
obj.prop = valи удаленияdelete obj.propвлияют только на сам объект. - При обращении к методу объекта, который находится в прототипе,
thisвсё равно ставится на сам объект.
Управление __proto__:
- Firefox/Chrome дают полный доступ к
obj.__proto__. Эта нестандартная возможность бывает полезна в целях отладки. - Все современные браузеры, IE9+ предоставляют метод
Object.getPrototypeOf(obj)для чтения прототипа объектаobjиObject.create(proto)для создания объекта с данным прототипом. - Для всех браузеров — функция-конструктор при создании объекта устанавливает его
__proto__равным своемуprototype. - Для всех браузеров — вызов
Object.create(proto)можно эмулировать при помощи функцииinherit:function inherit(proto) { function F() {} F.prototype = proto; return new F; }
Перебор свойств:
- Цикл
for..inперебирает все свойства объекта, включая находящиеся в прототипе. - Метод
obj.hasOwnProperty(prop)возвращаетtrueтолько если свойствоpropпринадлежит объектуobj, но не его прототипу. Можно использовать его, чтобы получить только свойства самого объектаobj.
Комментарии
- Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
- Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
- Комментарии без смысла, с рекламой или не о статье вообще - удаляются.