Конспект JS-course

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

Источник: https://developer.mozilla.org/ru/docs/Web/JavaScript/Guide/Inheritance_and_the_prototype_chain

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

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

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

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

Объекты в 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(b){
    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 или прототипов нативных (т.е., базовых) объектов JavaScript.

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

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