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

В отличие от классического наследования, прототипное наследование не обязательно имеет дело с увеличивающимися уровнями абстракции, а именно:

  1. объект может быть как абстракцией реальной вещи (как и в классическом наследовании), так и прямой копией другого объекта (прототипа (Prototype));
  2. объекты могут быть созданы сами по себе, или они могут быть созданы из других объектов.

Если взять пример из классического наследования (например, «ТУФЛИ» => «МУЖСКИЕ ТУФЛИ» => «МУЖСКИЕ ЛЕТНИЕ ТУФЛИ»), то вряд ли удастся избежать иерархии абстракций:

Главное отличие указанного выше способа наследования от классического способа: shoes, mensShoes, mensSummerShoes — независимые объекты (просто одни объекты созданы от других, одни объекты наследуют свойства от других объектов). А при классическом наследование обобщения являются абстракциями абстракций вплоть до самого последнего потомка.

При использование парадигмы прототипного наследования программист имеет дело только с объектами и при этом у него есть возможность создавать сущности в одном (горизонтальном) уровне абстракции.

JavaScript позволяет комбинировать обе формы наследования для достижения очень гибкой системы повторного использования кода, что собственно почти всегда и происходит в реальном коде JavaScript. 

В общем случае, в JavaScript новые объекты могут создаваться следующими способами:

  1. пустой объект — с помощью литеральной нотации {} или методом Object.create(null);
  2. с помощью функции-конструктора (в т.ч. class, и далее — расширение через функциональное наследование);
  3. клонировать существующий объект и расширить его через прототипное наследование:
    • делегированием через прототипы (объект ссылается или делегирует другому объекту):
      1. с помощью Object.create();
      2. с использованием constructor();
    • конкатенацией объектов (объект формируется путем добавления новых свойств к объекту из других существующих объектов методом Object.assign()).

Делегирование через прототипы в JavaScript

Делегирование — это когда объект ссылается на другой объект или делегирует функциональность другому объекту. Например, прототипы JavaScript также являются делегатами: экземпляры массива перенаправляют встроенные методы массива на Array.prototype, объекты — на Object.prototype и т. д.

Делегирование с помощью Object.create()

Делегирование с помощью Object.create() позволяет наследовать всю функциональность родительского объекта:

Таким образом, дочерний объект objCrtPerson полностью ссылается на родительский объект newPerson и получает значения его свойств и методов (newPerson делегирует свою функциональность objCrtPerson).

Ещё пример

Переработанный материал отсюда

Фактически, создание объекта можно автоматизировать, объединив все это в один литерал объекта:

[свернуть]

Делегирование с использованием constructor()

Делегирование с использованием constructor() используется при функциональном наследовании в связи с тем, что методы родительского класса, вынесенные (определенные) в свойстве prototype, при функциональном наследовании не наследуются.

Делегирование с использованием constructor() включает в себя два этапа:

  1. функциональное наследование;
  2. перезапись прототипа и восстановление constructor() дочернего объекта.

Для решения данной проблемы необходимо:

  1. в prototype дочернего класса записать prototype родительского класса, используя метод Object.create();
  2. воcстановить значение constructor() дочернего класса, потерянное при перезаписи prototype.

Таким образом, код функции-конструктора дочернего класса будет следующим:

Расширение объектов конкатенацией

Конкатенативное наследование — это процесс наследования компонентов напрямую из одного объект в другой через копирование свойств. В JavaScript прототипы обычно создаются через примеси и в ES6 эту возможность предоставляет метод Object.assign()

Наследование с помощью конкатенации:

Пример наследования через классы с аналогичным результатом

Наследование через классы:

[свернуть]

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

Конкатенация решает проблему «хрупкого базового класса». 

Подробнее

См. Eric Elliott — «Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance«?

A — это базовый класс;

B —  унаследованный от A;

C наследуется от B;

D наследуется от B.

C вызывает super, который запускает код в B. В свою очередь, B вызывает super, который запускает код в A.

A и B содержат несвязанные функции, необходимые как C, так и D.

D — это новый вариант функциональности, и для него требуется несколько иное поведение в коде инициализации A, чем требуется C.

C и D , возможно, не требуют использования всех функций классов A и B, а просто хотят унаследовать некоторые свойства или методы, которые уже определены в A и B. Но, наследуя с помощью вызова super, вы не можете избирательно подходить к тому, что вы наследуете. Вы наследуете все: «… проблема объектно-ориентированных языков в том, что у них есть вся эта неявная среда, которую они носят с собой. Вы хотели банан, но получили гориллу, держащую банан и все джунгли» (Joe Armstrong — “Coders at Work”).

Наследуя с помощью конкатенации можно обойти эту проблему.

Представьте, что у вас есть функции вместо классов: feat1, feat2, feat3, feat4.

C нужны feat1 и feat3 , а D нужны feat1 , feat2 , feat4:

Теперь представьте, что вы обнаруживаете, что D требует немного другого поведения, чем feat1 .

Так как при использовании наследования классов вы получаете всю существующую таксономию классов, то если вы хотите немного адаптироваться для нового варианта использования, вам придется либо дублировать части существующей таксономии (проблема дублирования по необходимости), либо реорганизовать всё, что зависит от существующей таксономии, чтобы адаптировать таксономию к новому использованию.

При наследовании с помощью конкатенации на самом деле нет необходимости изменять feat1, вместо этого вы можете создать собственную версию feat1 и использовать ее. Вы по-прежнему можете наследовать существующее поведение от feat2 и feat4 без изменений:

И C остается неизменным.

[свернуть]
 

Добавить комментарий

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.