Наследование в JavaScript
=========================

* Назад
* Обзор: Objects
* Далее

Теперь, когда объясняется большая часть подробностей OOJS, эта статья показывает, как создавать «дочерние» классы объектов (конструкторы), которые наследуют признаки из своих «родительских» классов. Кроме того, мы дадим некоторые советы о том, когда и где вы можете использовать OOJS , и посмотрим, как классы рассматриваются в современном синтаксисе ECMAScript.

| Необходимые знания: | Базовая компьютерная грамотность, понимание основ HTML и CSS, знакомство с основами JavaScript (см. Первые шаги и Структурные элементы) and основы Объектно-ориентированного JS (см. Введение в объекты). |
| --- | --- |
| Цель: | Понять, как можно реализовать наследование в JavaScript. |

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

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

Давайте рассмотрим, как это сделать на конкретном примере.

Начало работы
-------------

Прежде всего сделайте себе локальную копию нашего файла oojs-class-inheritance-start.html (он также работает в режиме реального времени). В файле вы найдёте тот же пример конструктора `Person()`, который мы использовали на протяжении всего модуля, с небольшим отличием - мы определили внутри конструктора только лишь свойства:


```js
function Person(first, last, age, gender, interests) {
  this.name = {
    first,
    last,
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
}
```


*Все* методы определены в прототипе конструктора. Например:


```js
Person.prototype.greeting = function () {
  alert("Hi! I'm " + this.name.first + ".");
};
```


**Примечание:** . В исходном коде вы также увидите определённые методы `bio()` и `farewell()`. Позже вы увидите, как они могут быть унаследованы другими конструкторами.

Скажем так, мы хотели создать класс `Teacher`, подобный тому, который мы описали в нашем первоначальном объектно-ориентированном определении, которое наследует всех членов от `Person`, но также включает в себя:

1. Новое свойство, `subject` - оно будет содержать предмет, который преподаёт учитель.
2. Обновлённый метод `greeting()`, который звучит немного более формально, чем стандартный метод `greeting()` — более подходит для учителя, обращающегося к некоторым ученикам в школе.
Определение функции-конструктора Teacher()
------------------------------------------

Первое, что нам нужно сделать, это создать конструктор `Teacher()` - добавьте ниже следующий код:


```js
function Teacher(first, last, age, gender, interests, subject) {
  Person.call(this, first, last, age, gender, interests);

  this.subject = subject;
}
```


Это похоже на конструктор Person во многих отношениях, но здесь есть что-то странное, что мы не видели раньше - функцию `call()`. Эта функция в основном позволяет вам вызывать функцию, определённую где-то в другом месте, но в текущем контексте. Первый параметр указывает значение `this`, которое вы хотите использовать при выполнении функции, а остальные параметры - те, которые должны быть переданы функции при её вызове.

Мы хотим, чтобы конструктор `Teacher()` принимал те же параметры, что и конструктор `Person()`, от которого он наследуется, поэтому мы указываем их как параметры в вызове `call()`.

Последняя строка внутри конструктора просто определяет новое свойство `subject`, которое будут иметь учителя, и которого нет у Person().

В качестве примечания мы могли бы просто сделать это:


```js
function Teacher(first, last, age, gender, interests, subject) {
  this.name = {
    first,
    last,
  };
  this.age = age;
  this.gender = gender;
  this.interests = interests;
  this.subject = subject;
}
```


Но это просто переопределяет свойства заново, а не наследует их от `Person()`, так что теряется смысл того, что мы пытаемся сделать. Он также занимает больше строк кода.

### Наследование от конструктора без параметров

Обратите внимание, что если конструктор, от которого вы наследуете, не принимает значения своего свойства из параметров, вам не нужно указывать их в качестве дополнительных аргументов в `call()`. Так, например, если у вас было что-то действительно простое:


```js
function Brick() {
  this.width = 10;
  this.height = 20;
}
```


Вы можете наследовать свойства `width` и `height`, выполнив это (как и другие шаги, описанные ниже, конечно):


```js
function BlueGlassBrick() {
  Brick.call(this);

  this.opacity = 0.5;
  this.color = "blue";
}
```


Обратите внимание, что мы указали только `this` внутри `call()` - никаких других параметров не требуется, поскольку мы не наследуем никаких свойств родителя, которые задаются через параметры.

Установка Teacher()'s prototype и конструктор ссылок
----------------------------------------------------

Пока все хорошо, но у нас есть проблема. Мы определили новый конструктор и у него есть свойство `prototype`, которое по умолчанию просто содержит ссылку на саму конструкторскую функцию. Он не содержит методов свойства `prototype` конструктора `Person`. Чтобы увидеть это, введите `Object.getOwnPropertyNames(Teacher.prototype)` в поле ввода текста или в вашу консоль JavaScript. Затем введите его снова, заменив `Teacher` на `Person`. Новый конструктор *не наследует* эти методы. Чтобы увидеть это, сравните выводы в консоль `Person.prototype.greeting` и `Teacher.prototype.greeting`. Нам нужно заставить `Teacher()` наследовать методы, определённые на прототипе `Person()`. Итак, как мы это делаем?

1. Добавьте следующую строку ниже своего предыдущего добавления:

```js
Teacher.prototype = Object.create(Person.prototype);
```

Здесь наш друг `create()` снова приходит на помощь. В этом случае мы используем его для создания нового объекта и делаем его значением `Teacher.prototype`. Новый объект имеет свой прототип `Person.prototype` и, следовательно, наследует, если и когда это необходимо, все доступные методы `Person.prototype`.
2. Нам нужно сделать ещё одну вещь, прежде чем двигаться дальше. После добавления последней строки, `Teacher.prototype.constructor` стало равным `Person()`, потому что мы просто устанавливаем `Teacher.prototype` для ссылки на объект, который наследует его свойства от `Person.prototype`! Попробуйте сохранить код, загрузите страницу в браузере и введите `Teacher.prototype.constructor` в консоль для проверки.
3. Это может стать проблемой, поэтому нам нужно сделать это правильно. Вы можете сделать это, вернувшись к исходному коду и добавив следующие строки внизу:
```

Object.defineProperty(Teacher.prototype, 'constructor', {
    value: Teacher,
    enumerable: false, // false, чтобы данное свойство не появлялось в цикле for in
    writable: true });
```

4. Теперь, если вы сохраните и обновите, введите `Teacher.prototype.constructor`, чтобы вернуть `Teacher()`, плюс мы теперь наследуем `Person()`!
Предоставление Teacher() новой функции greeting()
-------------------------------------------------

Чтобы завершить наш код, нам нужно определить новую функцию `greeting()` в конструкторе `Teacher()`.

Самый простой способ сделать это - определить его на прототипе `Teacher()` - добавить в нижнюю часть кода следующее:


```js
Teacher.prototype.greeting = function () {
  var prefix;

  if (
    this.gender === "male" ||
    this.gender === "Male" ||
    this.gender === "m" ||
    this.gender === "M"
  ) {
    prefix = "Mr.";
  } else if (
    this.gender === "female" ||
    this.gender === "Female" ||
    this.gender === "f" ||
    this.gender === "F"
  ) {
    prefix = "Mrs.";
  } else {
    prefix = "Mx.";
  }

  alert(
    "Hello. My name is " +
      prefix +
      " " +
      this.name.last +
      ", and I teach " +
      this.subject +
      ".",
  );
};
```


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

Попробуйте пример
-----------------

Теперь, когда вы ввели весь код, попробуйте создать экземпляр объекта из `Teacher()`, поставив ниже вашего JavaScript-кода (или что-то похожее по вашему выбору):


```js
var teacher1 = new Teacher(
  "Dave",
  "Griffiths",
  31,
  "male",
  ["football", "cookery"],
  "mathematics",
);
```


Теперь сохраните, обновите, и попробуйте получить доступ к свойствам и методам вашего нового объекта `teacher1`, например:


```js
teacher1.name.first;
teacher1.interests[0];
teacher1.bio();
teacher1.subject;
teacher1.greeting();
teacher1.farewell();
```


Все должно работать нормально. Запросы в строках 1, 2, 3 и 6 унаследованные от общего конструктора `Person()` (класса). Запрос в строке 4 обращается к `subject`, доступному только для более специализированного конструктора (класса) `Teacher()`. Запрос в строке 5 получил бы доступ к методу `greeting()`, унаследованному от `Person()`, но `Teacher()` имеет свой собственный метод `greeting()` с тем же именем, поэтому запрос обращается к этому методу.

**Примечание:** . Если вам не удаётся заставить это работать, сравните свой код с нашей готовой версией (см. также рабочее демо).

Методика, которую мы здесь рассмотрели, - это не единственный способ создания наследующих классов в JavaScript, но он работает нормально и это даёт вам представление о том, как реализовать наследование в JavaScript.

Вам также может быть интересно узнать некоторые из новых функций ECMAScript, которые позволяют нам делать наследование более чисто в JavaScript (см. Classes). Мы не рассматривали их здесь, поскольку они пока не поддерживаются очень широко в браузерах. Все остальные конструкторы кода, которые мы обсуждали в этом наборе статей, поддерживаются ещё в IE9 или ранее и есть способы добиться более ранней поддержки, чем это.

Обычный способ - использовать библиотеку JavaScript - большинство популярных опций имеют простой набор функций, доступных для выполнения наследования более легко и быстро. CoffeeScript , например, предоставляет класс, расширяет и т.д.

Дальнейшее упражнение
---------------------

В нашем руководстве по Объектно-ориентированному JavaScript для начинающих (en-US) мы также включили класс `Student` как концепцию, которая наследует все особенности `Person`, а также имеет другой метод `greeting()` от `Person`, который гораздо более неформален, чем приветствие `Teacher`. Посмотрите, как выглядит приветствие ученика в этом разделе, и попробуйте реализовать собственный конструктор `Student()`, который наследует все функции `Person()` и реализует другую функцию `greeting()`.

**Примечание:** . Если вам не удаётся заставить это работать, сравните свой код с нашей готовой версией (см. также рабочее демо).

Object member summary
---------------------

Подводя итог, вы в основном получили три типа свойств / методов, о которых нужно беспокоиться:

1. Те, которые определены внутри функции-конструктора, которые присваиваются экземплярам объекта. Их довольно легко заметить - в вашем собственном коде они представляют собой элементы, определённые внутри конструктора, используя строки `this.x = x`; в встроенном коде браузера они являются членами, доступными только для экземпляров объектов (обычно создаются путём вызова конструктора с использованием ключевого слова `new`, например `var myInstance = new myConstructor ()`.
2. Те, которые определяются непосредственно самим конструктором, которые доступны только для конструктора. Они обычно доступны только для встроенных объектов браузера и распознаются путём непосредственной привязки к конструктору, а не к экземпляру. Например, `Object.keys()`.
3. Те, которые определены в прототипе конструктора, которые наследуются всеми экземплярами и наследуют классы объектов. К ним относятся любой член, определённый в свойстве прототипа конструктора, например. `myConstructor.prototype.x()`.

Если вы не уверены, что это такое, не беспокойтесь об этом, пока вы ещё учитесь и знание придёт с практикой.

Когда вы используете наследование в JavaScript?
-----------------------------------------------

В частности, после этой последней статьи вы можете подумать: «У-у-у, это сложно». Ну, ты прав. Прототипы и наследование представляют собой некоторые из самых сложных аспектов JavaScript, но многие возможности и гибкость JavaScript вытекают из его структуры объектов и наследования и стоит понять, как это работает.

В некотором смысле вы используете наследование все время. Всякий раз, когда вы используете различные функции веб-API или методы/свойства, определённые во встроенном объекте браузера, который вы вызываете в своих строках, массивах и т.д., вы неявно используете наследование.

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

**Примечание:** . Из-за того, как работает JavaScript, с цепочкой прототипов и т.д., совместное использование функций между объектами часто называется **делегированием**. Специализированные объекты делегируют функциональность универсальному типу объекта.

При использовании наследования вам рекомендуется не иметь слишком много уровней наследования и тщательно отслеживать, где вы определяете свои методы и свойства. Можно начать писать код, который временно изменяет прототипы встроенных объектов браузера, но вы не должны этого делать, если у вас нет действительно веской причины. Слишком много наследования могут привести к бесконечной путанице и бесконечной боли при попытке отладки такого кода.

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

Резюме
------

В этой статье мы рассмотрели оставшуюся часть основной теории и синтаксиса OOJS, которые, как мы думаем, вам следует знать сейчас. На этом этапе вы должны понимать основы JavaScript, ООП, прототипы и прототипное наследование, как создавать классы (конструкторы) и экземпляры объектов, добавлять функции в классы и создавать подклассы, которые наследуются от других классов.

В следующей статье мы рассмотрим, как работать с JavaScript Object Notation (JSON), общим форматом обмена данными, написанным с использованием объектов JavaScript.

See also
--------

* ObjectPlayground.com — A really useful interactive learning site for learning about objects.
* Secrets of the JavaScript Ninja, Chapter 6 — A good book on advanced JavaScript concepts and techniques, by John Resig and Bear Bibeault. Chapter 6 covers aspects of prototypes and inheritance really well; you can probably track down a print or online copy fairly easily.
* You Don't Know JS: this & Object Prototypes — Part of Kyle Simpson's excellent series of JavaScript manuals, Chapter 5 in particular looks at prototypes in much more detail than we do here. We've presented a simplified view in this series of articles aimed at beginners, whereas Kyle goes into great depth and provides a more complex but more accurate picture.
* Назад
* Обзор: Objects
* Далее