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

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

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

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

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

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

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


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

// Допустим, у нас есть объект 'o' с собственными свойствами a и b
// {a:1, b:2}

// o.[[Prototype]] имеет свойства b и с
// {b:3, c:4}

// Далее, o.[[Prototype]].[[Prototype]] является null
// null - это окончание в цепочке прототипов
// по определению, null не имеет свойства [[Prototype]]

// В итоге полная цепочка прототипов выглядит так:
// {a:1, b:2} ---> {b:3, c:4} ---> null

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

console.log(o.b); // 2
// Есть ли у объекта 'o' собственное свойство 'b'?
// Да, и его значение равно 2.
// У прототипа o.[[Prototype]] также есть свойство '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
```


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

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

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

При выполнении унаследованной функции значение `this`(/ru/docs/Web/JavaScript/Reference/Operators/this) указывает на объект-потомок, а не на прототип, в котором функция является собственным свойством.


```js
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'
```

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

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


```js
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.


```js
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. Прототип создаваемого объекта указывается в первом аргументе этого метода:


```js
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
```

### Используя ключевое слово `class`

С выходом ECMAScript 6 появился целый набор ключевых слов, реализующих классы. Они могут показаться знакомыми людям, изучавшим языки, основанные на классах, но есть существенные отличия. JavaScript был и остаётся прототипно-ориентированным языком. Новые ключевые слова: "`class`", "`constructor`", "`static`", "`extends`" и "`super`".


```js
"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height \* this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);
```

### Производительность

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

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

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

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

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

### Плохая практика: расширение базовых прототипов

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

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

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

Примеры
-------

`B` наследует от `A`:


```js
function A(a) {
  this.varA = a;
}

// What is the purpose of including varA in the prototype when A.prototype.varA will always be shadowed by
// this.varA, given the definition of function A above?
A.prototype = {
  varA: null, // Shouldn't we strike varA from the prototype as doing nothing?
  // perhaps intended as an optimization to allocate space in hidden classes?
  // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
  // would be valid if varA wasn't being initialized uniquely for each instance
  doSomething: function () {
    // ...
  },
};

function B(a, b) {
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB: {
    value: null,
    enumerable: true,
    configurable: true,
    writable: true,
  },
  doSomething: {
    value: function () {
      // переопределение
      A.prototype.doSomething.apply(this, arguments); // call super
      // ...
    },
    enumerable: true,
    configurable: true,
    writable: true,
  },
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();
```


Важно:

* Типы определяются в `.prototype`
* Для наследования используется `Object.create()`
prototype и Object.getPrototypeOf
---------------------------------

Как уже упоминалось, JavaScript может запутать разработчиков на Java или C++, ведь в нём совершенно нет "нормальных" классов. Всё, что мы имеем - лишь объекты. Даже те "classes", которые мы имитировали в статье, тоже являются функциональными объектами.

Вы наверняка заметили, что у `function A` есть особое свойство `prototype`. Это свойство работает с оператором `new`. Ссылка на объект-прототип копируется во внутреннее свойство `[[Prototype]]` нового объекта. Например, в этом случае `var a1 = new A()`, JavaScript (после создания объекта в памяти и до выполнения функции function `A()`) устанавливает `a1.[[Prototype]] = A.prototype`. Потом, при попытке доступа к свойству нового экземпляра объекта, JavaScript проверяет, принадлежит ли свойство непосредственно объекту. Если нет, то интерпретатор ищет в свойстве `[[Prototype]]`. Всё, что было определено в `prototype,` в равной степени доступно и всем экземплярам данного объекта. При внесении изменений в `prototype` все эти изменения сразу же становятся доступными и всем экземплярам объекта.

`[[Prototype]]` работает *рекурсивно*, то есть при вызове:


```js
var o = new Foo();
```


JavaScript на самом деле выполняет что-то подобное:


```js
var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);
```


а когда вы делаете так:


```js
o.someProp;
```


 JavaScript проверяет, есть ли у **`o`** свойство `someProp`.
 и если нет, то проверяет `Object.getPrototypeOf(o).someProp`
 а если и там нет, то ищет в `Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp` и так далее.

Заключение
----------

 Важно чётко понимать принципы работы прототипной модели наследования, прежде чем начинать писать сложный код с её использованием.
 При написании JavaScript-кода, использующего наследование, следует помнить о длине цепочек прототипов и стараться делать их как можно более короткими во избежание проблем с производительностью во время выполнения кода.
 Расширять базовые прототипы следует исключительно для поддержания совместимости кода с отдельными "древними" реализациями JavaScript, - во всех прочих случаях это плохая практика.