Я видел, как разработчики, пришедшие из традиционных ООП-языков, были встревожены, более того, в ужасе от того, что в JavaScript нельзя сделать следующее:

class MyClass extends SomeClass, YourClass, NoClass {
    // rest of class here
}

«Что за ‹любимое-ругательство-здесь›» этот язык», — жалуются они на Reddit, в Stack Overflow или в JS-блоге дяди Билли. «Это не настоящий язык. Кто придумал это «другое-любимое-ругательство-здесь»?»

Конечно, я просто фейспалм, потому что знаю внутренние секреты JavaScript. Красота языка. Несколько расширений? Нам не нужны вонючие множественные расширения!

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

Прежде чем мы начнем

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

Это не по пути. Давайте приступим!

В нашем примере мы будем конструировать животных из разных частей.

Вот что мы хотим:

class Cat extends AnimalBase, Body, Head, Tail, Legs) {
 constructor() {
  super();
  this.legs = 4;
 }
}

Вот как это будет выглядеть:

class Cat extends extender(body, head, tail, legs) {
 constructor() {
  super();
  this.legs = 4;
 }
}

Как видите, наш код очень мало отличается от кода цели. Только то, что мы продлили. Но результаты такие же. Наше животное будет иметь свойства и методы, связанные с AnimalBase, Body, Head, Tail и Legs.

Разница в этой функции extender(). Вместо того, чтобы расширять другой класс, мы будем расширять функцию, которая возвращает класс. И вот эта функция:

const extender = (...parts) => parts.reduce(creator, BaseAnimal);

Ждать. Действительно? Вот и все? Да, это все, что нужно. Именно здесь в игру вступает магия метода Array.reduce().

Массив.уменьшить()

Метод Array.reduce() перебирает массив, применяя каждый элемент к функции, которую вы передаете ему в качестве первого аргумента в объявлении. Затем результат этой функции запоминается от одной итерации к другой и передается вашей функции с каждой итерацией. Этот результат называется аккумулятор. Когда все элементы будут обработаны, окончательным результатом будет значение, возвращаемое методом Array.reduce(). Я сделал запись в блоге и видео в своем блоге Boring JavaScript о методе Array.reduce(), если вам нужна дополнительная информация.

В нашем примере «creator» — это функция для метода Array.reduce(), а «BaseAnimal» — это предопределенный класс, передаваемый в качестве начального значения аккумулятора. «части» — это аргументы, переданные функции, преобразованные в массив с помощью оператора «остаток».

Давайте посмотрим на эту функцию «создатель»:

// 'allAnimalParts' is the accumulator
// 'animalPart' is the element of the array
const creator = (allAnimalParts, animalPart) => animalPart(allAnimalParts);

Ваши глаза вас не обманывают — вот и все. Простой лайнер, который использует «текущее значение» функции редуктора (элемент массива) в качестве имени вызываемой функции, передавая ей «аккумулятор» функции редуктора(). Результат этого передается обратно функции редуктора в качестве «аккумулятора», который будет использоваться для следующей итерации (или для возврата в вызывающий код, когда итерация завершена).

И результаты этой функции? Почему, определения классов.

Истинная магия в классе

Где мы сейчас находимся? Наша функция-редуктор вызывает функцию, в которой ей передается объект. Эта функция-редуктор возвращает определение классадля каждого элемента и конечное значение функции-редуктора().

Подождите: возвращает определение класса? Да! Но как?

Все в JavaScript является объектом (ну, кроме простых значений). Массив — это объект. Функция — это объект. А определение класса — это объект (определение класса на самом деле не что иное, как синтаксический сахар для объекта-функции). Вы можете передавать объекты в качестве аргументов функций, верно? Это означает, что вы можете передавать массивы или функции или… да… даже классы.

И в этом магия этого дизайна. Давайте посмотрим, как теперь устроен класс:

function body(Base) {
 class Body extends Base {
  constructor() {
   super();
   this.body = true;
  }
 }
 return Body;
}

Вместо «тела» в качестве простого определения класса теперь у нас есть функция «тело», которая возвращает определение класса. И что расширяет этот класс? Он расширяет аргумент, переданный функции, который также является определением класса.

В конечном итоге эта функция возвращает класс, который был расширен аргументом функции, которая является другим классом.

А теперь — грандиозный финал

Давайте объединим все это.

Мы знаем, что у нас есть функция, которая возвращает определение класса. Эта функция принимает в качестве аргумента другое определение класса. И наша функция редуктора вызывает функцию, которая возвращает определение класса, чтобы в конечном итоге вернуть его как наше окончательное определение класса. Смущенный? Давайте составим код и рассмотрим его шаг за шагом.

class BaseAnimal {
 constructor() {
  this.base = true;
 }
}

const creator = (allAnimalParts, animalPart) => animalPart(allAnimalParts);
const extender = (...parts) => parts.reduce(creator, BaseAnimal);

class Cat extends extender(body, head, tail, legs) {
 constructor() {
  super();
  this.legs = 4;
 }
}
function body(Base) {
 class Body extends Base {
  constructor() {
   super();
   this.body = true;
  }
 }
 return Body;
}
// all the other functions for header, tail, legs, etc. follow 
// the same schema as 'body' above.

Когда мы вызываем «классCat extends extender(тело, голова, хвост, ноги)», получаем следующий поток:

  1. Вызывается функция «расширитель», которая использует Array.reduce() для вызова функции «создатель» для каждого элемента.
  2. Начальное значение для функции редуктора («аккумулятор») установлено для класса «BaseAnimal».
  3. «Создатель» выполняется с вызовом функции «тело» с использованием «аккумулятора» в качестве аргумента. Поскольку «аккумулятор» является классом «BaseAnimal», «класс Body extends BaseAnimal» возвращается как класс в «аккумулятор».
  4. Далее выполняется «создатель» с вызовом функции «голова» с использованием «аккумулятора» в качестве аргумента. «аккумулятор» — это «класс Body extends BaseAnimal», поэтому функция «head» возвращает результат, расширяя Head с помощью «аккумулятора» — в основном — «класс Head extends (Body extends BaseAnimal)».
  5. «Создатель» выполняется с помощью функции «хвост». Возврат этой функции концептуально выглядит так: «класс Tail extends (Head extends (Body extends BaseAnimal))».
  6. «создать» выполняется с помощью функции «ноги». Возврат этой функции, опять же концептуально, таков: «класс Legs extends (Tail extends (Head extends (Body extends BaseAnimal)))».
  7. Наконец, функция «исполнитель» возвращает окончательное значение «аккумулятора» в определение нашего класса, давая нам — концептуально — «класс Кошка расширяется (Ноги расширяются (Хвост расширяется (Голова расширяется (Тело расширяет BaseAnimal))»).

Поскольку окончательное значение, возвращаемое нашей функцией «расширителя», является определением класса, с точки зрения JavaScript мы передаем только один класс. Однако конструкция этого класса строится слой за слоем, комбинируя свойства и методы, пока не будет построен единственный класс.

Конечный результат? Программно имеем вот это:

class Cat extends BaseAnimal, Body, Head, Tail, Legs {
    // rest of class here
}

И, таким образом, мы сделали несколько расширений в JavaScript.

И если вы запустите в console.log экземпляр класса Cat, вы получите:

Cat { base: true, body: true, head: true, tail: true, legs: 4 }

Это именно то, что мы хотели — несколько классов, объединенных в один класс.

Вывод

Опять же, мы действительно не использовали «множественное расширение» в JavaScript — это просто невозможно. Но то, что мы сделали, это смоделировали его с помощью Array.reduce(), чтобы предоставить нам одно определение класса, в котором есть другие классы.

Я надеюсь, что вы все еще с нами в этот момент — это много читать. Напишите мне, если у вас есть какие-либо вопросы.

Приложение: Окончательный код:

Вот весь код, который я использовал:

class BaseAnimal {
 constructor() {
  this.base = true;
 }
}

const creator = (allAnimalParts, animalPart) => animalPart(allAnimalParts);
const extender = (...parts) => parts.reduce(creator, BaseAnimal);

class Cat extends extender(body, head, tail, legs) {
 constructor() {
  super();
  this.legs = 4;
 }
}

class RoadRunner extends extender(body, head, tail, legs, wings) {
 constructor() {
  super();
  this.legs = 2;
  this.wings = 2;
 }
}

const myCat = new Cat();
console.log(myCat);
const myRoadRunner = new RoadRunner();
console.log(myRoadRunner);
// creating a new class from the functions below
const myArms = new (arms(BaseAnimal));
console.log(myArms);
function body(Base) {
 class Body extends Base {
  constructor() {
   super();
   this.body = true;
  }
 }
 return Body;
}

function legs(Base) {
 class Legs extends Base {
  constructor(num = 4) {
   super();
   this.legs = num;
  }
 }
 return Legs;
}
function arms(Base) {
 class Arms extends Base {
  constructor(num = 2) {
   super();
   this.arms = num;
  }
 }
 return Arms;
}

function head(Base) {
 class Head extends Base {
  constructor() {
   super();
   this.head = true;
  }
 }
 return Head;
}

function tail(Base) {
 class Tail extends Base {
  constructor() {
   super();
   this.tail = true;
  }
 }
 return Tail;
}

function wings(Base) {
 class Wings extends Base {
  constructor() {
   super();
   this.wings = true;
  }
 }
 return Wings;
}