Наследование и цепочка прототипов

Модель наследования в JavaScript может озадачить опытных разработчиков на высокоуровневых объектно-ориентированных языках (таких, например, как Java или C++), так как она динамическая и не включает в себя реализацию понятия class (хотя ключевое слово class и является зарезервированным, т.е., не может быть использовано в качестве имени переменной).

В плане наследования JavaScript работает лишь с одной сущностью: объектами. Каждый объект имеет внутреннюю ссылку на другой объект, называемый его прототипом. У объекта-прототипа также есть свой собственный прототип и так далее до тех пор, пока цепочка не завершится объектом, у которого свойство prototype равно null. null, по определению, не имеет прототипа и служит в качестве завершающего звена в цепочке прототипов.

Хотя прототипную модель наследования некоторые относят к недостаткам JavaScript, на самом деле она мощнее классической. К примеру, поверх неё можно предельно просто реализовать классическое наследование, а вот попытки совершить обратное непременно вынудят Вас попотеть.

Наследование с цепочкой прототипов

Наследование свойств

Объекты в JavaScript - это как бы динамические "контейнеры", наполненные свойствами (называемыми собственными свойствами) и у каждого объекта есть при этом ссылка на свой объект-прототип. При попытке получить доступ к какому-либо свойству объекта происходит следующее:

// Допустим, у нас есть объект 'o' с цепочкой прототипов выглядящей как:
// {a:1, b:2} ---> {b:3, c:4} ---> null
// где 'a' и 'b' - собственные свойства объекта 'o'.

// В этом примере someObject.[[Prototype]] означает прототип someObject.
// Это упрощённая нотация (описанная в стандарте ECMAScript). Она не может быть использована в скриптах.

console.log(o.a); // 1
// Есть ли у объекта 'o' собственное свойство 'a'? Да, и его значение равно 1

console.log(o.b); // 2
// Есть ли у объекта 'o' собственное свойство 'b'? Да, и его значение равно 2
// У прототипа тоже есть свойство 'b', но обращения к нему в данном случае не происходит. Это и называется "property shadowing"

console.log(o.c); // 4
// Есть ли у объекта 'o' собственное свойство 'с'? Нет, тогда поищем его в прототипе.
// Есть ли у объекта o.[[Prototype]] собственное свойство 'с'? Да, оно равно 4

console.log(o.d); // undefined
// Есть ли у объекта 'o' собственное свойство 'd'? Нет, тогда поищем его в прототипе.
// Есть ли у объекта o.[[Prototype]] собственное свойство 'd'? Нет, продолжаем поиск по цепочке прототипов.
// o.[[Prototype]].[[Prototype]] равно null, прекращаем поиск, свойство не найдено, возвращаем undefined

При добавлении к объекту нового свойства создаётся новое собственное свойство (own property). Единственным исключением из этого правила являются наследуемые свойства, имеющие getter или setter.

Наследование "методов"

JavaScript не имеет "методов" в смысле, принятом в классической модели ООП. В JavaScript любая функция может быть добавлена к объекту в виде его свойства. Унаследованная функция ведёт себя точно так же, как любое другое свойство объекта, в том числе и в плане "затенения свойств" (property shadowing), как показано в примере выше (в данном конкретном случае это форма переопределения метода - method overriding).

В области видимости унаследованной функции ссылка this указывает на наследуемый объект, а не на прототип, в котором данная функция является собственным свойством.

var o = {
  a: 2,
  m: function(){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// в этом случае при вызове 'o.m' this указывает на 'o'

var p = Object.create(o);
// 'p' - наследник 'o'

p.a = 12; // создаст собственное свойство 'a' объекта 'p'
console.log(p.m()); // 13
// при вызове 'p.m' this указывает на 'p'.
// т.е. когда 'p' наследует функцию 'm' объекта 'o', this.a означает 'p.a', собственное свойство 'a' объекта 'p'

Различные способы создания объектов и получаемые в итоге цепочки прототипов

Создание объектов с помощью литералов

var o = {a: 1};

// Созданный объект 'o' имеет Object.prototype в качестве своего [[Prototype]]
// 'o' не имеет собственное свойство 'hasOwnProperty'
// hasOwnProperty - это собственное свойство Object.prototype. Таким образом 'o' наследует hasOwnProperty от Object.prototype
// Object.prototype в качестве прототипа имеет null.
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// Массивы наследуются от Array.prototype (у которого есть такие методы, как indexOf, forEach и т.п.).
// Цепочка прототипов при этом выглядит так:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// Функции наследуются от Function.prototype (у которого есть такие методы, как call, bind и т.п.):
// f ---> Function.prototype ---> Object.prototype ---> null

Создание объектов с помощью конструктора

В JavaScript "конструктор" - это "просто" функция, вызываемая с оператором new.

function Graph() {
  this.vertexes = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertexes.push(v);
  }
}

var g = new Graph();
// объект 'g' имеет собственные свойства 'vertexes' и 'edges'.
// g.[[Prototype]] принимает значение Graph.prototype при выполнении new Graph().

Object.create

В ECMAScript 5 представлен новый метод создания объектов: Object.create. Прототип создаваемого объекта указывается в первом аргументе этого метода:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (унаследовано)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, т.к. 'd' не наследуется от Object.prototype

Эффективность

Длительное время поиска свойств, располагающихся относительно высоко в цепочке прототипов, может негативно сказаться на эффективности, особенно в критических в этом смысле местах кода. Кроме того, попытка найти несуществующие свойства неизбежно приведет к проверке на их наличие у всех объектов цепочки прототипов.

Кроме того, при циклическом переборе свойств объекта будет обработано каждое свойство, присутствующее в цепочке прототипов.

Если вам необходимо проверить, определено ли свойство у самого объекта, а не где-то в его цепочке прототипов, вы можете использовать метод hasOwnProperty, который все объекты наследуют от Object.prototype.

hasOwnProperty — единственная существующая в JavaScript возможность работать со свойствами, не затрагивая цепочку прототипов. 

Примечание: Для проверки существования свойства недостаточно проверять, эквивалентно ли оно undefined. Свойство может вполне себе существовать, но при этом ему может быть присвоено значение undefined.

Нехорошая практика: Расширение базовых прототипов

Одной из частых ошибок является расширение Object.prototype или других базовых прототипов.

Эта технология, называемая monkey patching, нарушает принцип инкапсуляции. Несмотря на то, что она используется в широко распространенных фреймворках, таких как Prototype.js, на настоящий момент не существует разумных причин для ее использования, та как в данном случае встроенные типы "захламляются" дополнительной нестандартной функциональностью.

Единственным оправданием расширения базовых прототипов является лишь эмуляция новых возможностей, таких как Array.forEach, для неподдерживающих их старых версий языка.

Заключение

Перед тем как приступать к написанию сложного кода с использованием прототипной модели наследования, важно иметь четкое представление о том, как она работает. Также, во избежание возможных проблем с эффективностью вашего кода, не следует забывать о длине цепочек прототипов, укорачивая их при каждом удобном случае. Кроме того, не следует расширять базовые прототипы иначе как в целях обеспечения совместимости с более поздними версиями JavaScript.

Метки документа и участники

Обновлялась последний раз: Kapiroska,
Скрыть боковую панель