🧬 Как реализовать наследование в JavaScript: 7 способов

Исчерпывающий гайд по всем существующим способам организации наследования в JavaScript. Разберем сильные и слабые стороны каждого подхода и научимся выбирать оптимальный метод для твоих задач.

Этот материал взят из нашей еженедельной email-рассылки, посвященной фронтенду. Подпишитесь, чтобы быть в числе первых, кто получит дайджест.

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

  • Наследование через цепочку прототипов.
  • Наследование через конструктор.
  • Композиционное наследование.
  • Паразитическое наследование.
  • Прототипное наследование.
  • Паразитическое композиционное наследование.
  • Наследование через классы ES6.

Давайте разберем эти техники и поймем, в каких случаях их лучше применять.

«Друг выучил COBOL и получил кодовую базу, в которой последние изменения были сделаны в 90-х… его собственной мамой». «Вообще-то наследование в программировании должно работать по-другому».

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

Это один из самых простых методов наследования в JavaScript – он позволяет объекту наследовать свойства и методы через цепочку, соединенную прототипами:

Принцип работы:

  • Конструктор Animal создает объект с базовыми свойствами – species и массивом habits.
  • Конструктор Dog добавляет специфическое свойство – породу (breed).
  • Dog.prototype = new Animal() предоставляет объектам Dog доступ к свойствам и методам объекта Animal через цепочку прототипов.

Преимущества:

  • Не требует сложной логики для связи объектов.
  • Подклассы могут использовать методы и свойства, определенные в родительском классе.

Недостатки:

  • Проблема с общими ссылочными типами. Если изменяется массив или объект (например, habits в примере), это изменение отразится на всех экземплярах, потому что они используют одну и ту же ссылку на этот объект:
d1.habits.push('bark');
console.log(d2.habits); // ['sleep', 'eat', 'bark']
  • Конструктор родителя вызывается для каждого объекта. Каждый раз при создании экземпляра подкласса new Dog() создается новый экземпляр родительского конструктора, что может сказаться на производительности, если он выполняет сложную логику.

Наследование через конструктор

Этот подход позволяет дочерним объектам наследовать свойства родителя, вызывая его конструктор напрямую внутри конструктора дочернего объекта:

Принцип работы:

  • Animal.call(this, 'Mammal'); передает контекст текущего объекта this в конструктор Animal. В результате свойства species и activities, определенные в Animal, становятся частью объекта Dog.
  • Конструктор Dog добавляет новое свойство breed, уникальное для каждого экземпляра.

Преимущества:

  • Уникальные свойства для каждого экземпляра. В отличие от цепочки прототипов, массивы или объекты (например, activities) не разделяются между экземплярами, что устраняет побочные эффекты.
  • Передача параметров родителю. Можно передать значения в конструктор родителя Animal через call, например, new Animal('Reptile') создаст экземпляр пресмыкающегося животного.

Недостатки:

  • Методы родительского конструктора не наследуются. Например, если в Animal есть метод, он не будет доступен в Dog, и это приведет к избыточности, если методы нужно дублировать в каждом наследнике.
  • Каждый вызов call создает новые копии свойств и методов, что может быть ресурсозатратно.

Композиционное наследование

Композиционное (составное) наследование объединяет цепочку прототипов и конструкторное наследование, позволяя дочерним классам иметь уникальные свойства и доступ к методам родителя:

Принцип работы:

  • Конструкторное наследование Animal.call(this, 'Mammal') – свойства из конструктора Animal копируются в экземпляры Dog. Например, species и activities становятся уникальными для каждого экземпляра.
  • Прототипное наследование Dog.prototype = new Animal() – методы Animal.prototype (например, getSpecies) становятся доступными для объектов Dog.
  • Восстановление конструктора Dog.prototype.constructor = Dog – после замены прототипа ссылка на конструктор нарушается. Эта строка восстанавливает корректное значение, что важно для работы с instanceof и идентификации классов.

Преимущества:

  • Каждый экземпляр имеет свои собственные свойства, что устраняет проблему общих ссылочных типов.
  • Дочерний класс может использовать методы, определенные в Animal.prototype, например, getSpecies.

Недостатки:

  • Дважды вызывается конструктор родителя.
  • Требуется больше кода и восстановление ссылки на конструктор.

Паразитическое наследование

Этот подход включает создание объекта на основе другого, его модификацию и возврат, что делает наследование гибким, но усложняет код:

Принцип работы:

  • Вместо прямого вызова конструктора родителя new Animal(), метод Object.create создает новый объект, унаследованный от Animal.prototype. Это более гибкий способ, который позволяет избежать вызова конструктора Animal на этом этапе.
  • Вызов Animal.call(this) в конструкторе Dog копирует уникальные свойства Animal (например, species и habits) в каждый экземпляр Dog.
  • После установки Dog.prototype можно добавлять собственные методы, такие как bark.

Преимущества:

  • Позволяет легко расширять прототипы и модифицировать унаследованные объекты. Например, можно добавлять новые методы без изменения родительского конструктора.
  • Использование Object.create позволяет избежать повторного вызова конструктора родителя при установке прототипа, что делает этот метод более производительным по сравнению с композиционным наследованием.

Недостатки:

  • В системах с глубоким уровнем наследования такой подход может усложнить поддержку кода.

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

Этот подход использует метод Object.create для прямого создания объекта с заданным прототипом, без использования конструкторов:

Принцип работы:

  • Object.create(obj) создает новый объект, прототипом которого является объект obj. В данном случае объект dog наследует свойства и методы объекта animal.
  • Метод getTraits добавляется в объект dog, что позволяет расширять функциональность клона без изменения прототипа animal.

Преимущества:

  • Не требуется создание конструкторов или сложных наследственных структур.
  • Подходит для создания простых объектов, унаследованных от базового объекта.

Недостатки:

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

Паразитическое композиционное наследование

Этот метод является улучшенной версией композиционного наследования – он устраняет проблему двойного вызова конструктора родителя, копируя свойства с помощью Object.create:

Принцип работы:

  • Вместо вызова new Animal() для создания прототипа дочернего объекта используется Object.create(Animal.prototype). Это позволяет унаследовать методы родителя без выполнения его конструктора.
  • Внутри конструктора Dog вызывается Animal.call(this), что обеспечивает копирование уникальных свойств (species, traits) в текущий экземпляр.

Преимущества:

  • Родительский конструктор вызывается только один раз – через Animal.call(this) в дочернем конструкторе. Это делает метод более производительным по сравнению с составным наследованием.
  • Дочерние объекты имеют уникальные свойства и доступ к методам, унаследованным от прототипа родителя.
  • Использование Object.create упрощает создание сложных наследственных структур.

Недостатки:

  • Требуется больше кода и восстановление ссылки на конструктор.
  • Включение метода Object.create и вызов родительского конструктора могут затруднить чтение и поддержку кода.
👨‍💻🎨 Библиотека фронтендера
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека фронтендера»

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

С появлением ES6 в JavaScript был введен синтаксис классов, который делает процесс наследования более понятным и удобным для разработчиков, привыкших к ООП:

Принцип работы:

  • Ключевое слово class определяет новый класс. В этом примере class Animal создает базовый класс с конструктором и методами.
  • Класс Dog наследует свойства и методы класса Animal, используя ключевое слово extends.
  • В конструкторе дочернего класса вызывается super, чтобы передать значения в конструктор родителя и выполнить его логику. Это обязательный шаг перед использованием this в конструкторе дочернего класса.
  • Методы, такие как getSpecies, добавляются в Animal.prototype и автоматически доступны объектам, созданным дочерним классом Dog.

Преимущества:

  • Чистый и понятный синтаксис, свойственный ООП.
  • Ключевые слова extends и super упрощают код.

Недостатки:

  • Для поддержки старых браузеров код на основе ES6 классов может потребовать использования Babel или других транспайлеров.
  • Классы ES6 слегка уступают по производительности прямым прототипным методам из-за дополнительных абстракций.

В заключение

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

Какой метод наследования вы чаще всего используете в своих проектах и почему? Поделитесь своим опытом!

***

Хочешь научиться создавать современные веб-интерфейсы и построить карьеру в IT? На курсе Frontend Basic ты освоишь HTML, CSS, JavaScript и React под руководством разработчиков из Газпромбанка и Аэрофлота, создашь 4 проекта в портфолио и получишь навыки, востребованные на рынке.

Источники

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ