Мастер-классы по Javascript Екатеринбург Ростов-на-Дону Москва Узнать больше...
Содержание (скрыть) Содержание (показать)

Прототипное наследование

  1. Наследование: ссылка __proto__
  2. Прототип и this
  3. Цепочка прототипов
  4. Установка и чтение прототипа в ES5
  5. Кросс-браузерное наследование
    1. Свойство prototype
    2. Функция inherit для эмуляции Object.create(proto)
    3. Метод hasOwnProperty
    4. Цикл по свойствам с унаследованными и без них
  6. Итого

Наследование — это способ расширения функциональности существующего объекта, без его изменения.

Для этого создаётся новый объект («потомок») и указывается, что он «наследует» от существующего («родителя»).

В дальнейшем, при обращении к свойству потомка, интерпретатор сначала проверяет, есть ли в нём это свойство, и если нет — ищет в родителе. Таким образом получается, что потомок имеет доступ ко всем свойствам родителя, но может добавить к ним свои.

Наследование: ссылка __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 — запись идет напрямую в объект.

Ссылка __proto__ в спецификации

В англоязычной спецификации 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():

  1. Интерпретатор ищет rabbit.eat, чтобы его вызвать. Но свойство eat отсутствует в объекте rabbit, поэтому он идет по ссылке rabbit.__proto__ и находит это свойство там.
  2. Функция eat запускается. Контекст ставится в равным объекту перед точкой, т.е. this = rabbit.

    Итак — получается, что команда this.full = true устанавливает свойство full в самом объекте rabbit:

Что мы имеем? Вызывается метод родителя, но при этом в контексте самого объекта. И это удобно! Особенно если наследование многоуровневое..

Цепочка прототипов

У объекта, который является __proto__, может быть свой __proto__, у того — свой, и так далее.

При доступе к свойству, интерпретатор будет искать его по цепочке:

  1. В самом объекте,
  2. В его прототипе __proto__,
  3. В __proto__ его прототипа,
  4. … и так далее: 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».

Значением prototype может быть только объект

Примитивное значение, такое как число или строка, будет проигнорировано.

Каков будет результат выполнения этого кода? Почему?

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)
}

  1. Создана новая функция F. Она ничего не делает с this, так что вызов new F вернёт пустой объект.
  2. Свойство F.prototype устанавливается в будущий прототип proto
  3. Результатом вызова new F будет пустой объект с __proto__ равным значению F.prototype.
  4. Готово! Мы получили пустой объект с заданным прототипом.

Эта функция широко используется в библиотеках и фреймворках.

Есть функция 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.

Комментарии

  1. Приветствуются комментарии, содержащие дополнения и вопросы по статье, и ответы на них.
  2. Если ваш комментарий касается задачи -- откройте её в отдельном окне и напишите там.
  3. Комментарии без смысла, с рекламой или не о статье вообще - удаляются.
Наверх

Содержание

Реклама

Нашли опечатку?

Нашли опечатку на сайте? Что-то кажется странным?
Выделите соответствующий текст и нажмите Ctrl+Enter!

Последние Комментарии

Помоги другим!

Помоги другим узнать о хорошей статье!