«Множественное наследование» в JavaScript

JavaScript
«Множественное наследование» в JavaScript

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

1. Долгожданный синтаксический сахар

До появления ES6, до использования React, Vue и других фреймворков, когда мы делали несколько сложных интерфейсных страниц и компонентов, мы часто использовали модульное мышление для инкапсуляции некоторой многократно используемой логики, и мы думали о том, чтобы предоставить JavaScript с поддержка «классов» в сочетании с некоторыми шаблонами проектирования позволяет создавать различные гибкие структуры кода.

Мы знаем, что JavaScript некласс не существует, Здесь есть толькоСеть прототипов, все используют функции и прототипы для инкапсуляции некоторых вещей для имитации «классов». Возможно, любую функцию можно считать «классом», если хотите.

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

[Рисунок 1] Цепочка прототипов JavaScript

Все эти годы мы ждали синтаксического сахара для «классов». . .

1.1 Макет класса

В строго типизированных языках классы объектно-ориентированы, поэтому мы должны упомянуть три основные характеристики [инкапсуляция] [наследование] [полиморфизм]

var Book = (function() {
  // 私有静态属性
  var privateStaticAttribute = 0;
  // 私有静态方法
  var privateStaticMethod = function() {};
  // 构造函数
  return function(props) {
    // 私有属性
    var title;
    // 私有方法
    this.getTitle = function() { return title; };
    this.setTitle = function(title) {};
  }
})();
// 公有静态方法
Book.staticMethod = function() {};
// 公有方法
Book.prototype.publicSharedMethod = function() {};

Этот вид кода должен быть очень знакомым.Он основан на понятии "класс" в строго типизированных языках.Поскольку это класс, помимо инкапсуляции некоторых свойств и методов, он также должен управлять видимостью. Поскольку нетмодификатор видимости, только замыкания могут использоваться для имитации общедоступных и частных. Несмотря на то, что по сравнению с классами в Java/C++ по-прежнему существует много недостатков, достигается по крайней мере некоторая инкапсуляция, и обычно мы также можем устанавливать соглашения об именах, соглашаясь, что имена свойств или имена методов, начинающиеся с подчеркивания, являются закрытыми.

С [инкапсуляцией] мы должны учитывать [наследование]. Однако в JavaScript нет механизма наследования, он используетprototypeДля имитации существует множество способов реализации, а также различные методы «наследования». Наследование прототипов, наследование классов и даже насмешкиsuperключевые слова, указатьClass.extend(),this.super()и другое удобное использование, все используют затворы иprototypeРеализованный синтаксис сахар. это прошлоеPrototype.jsВлияние, которое такая библиотека оказывает на внешний интерфейс.

Что касается [полиморфизма], то это нужно учитывать только в строго типизированных языках, когда тип объекта не может быть определен во время компиляции, он может быть определен только во время выполнения, где должна быть получена функция. Общий сценарий приложения: используйте ссылку супертипа для получения объекта подтипа, используйте функции, определенные в супертипе, для унифицированного управления объектами разных подтипов, и подкласс может переопределить функции в супертипе. Так уж получилось, что слабая особенность типов JavaScript, нет необходимости определять тип во время компиляции и, естественно, поддерживается полиморфизм.

1.2 После ES5

ES5 имеетObject.create(), будем удобнее использовать прототипное наследование,Object.getPrototypeOf,Object.setPrototypeOfЦепочкой прототипов можно манипулировать более свободно.

var Book = function(title) {
  Object.defineProperty(this, 'title', {
    writable: false,
    value: title
  });
};
Book.prototype.getTitle = function() { return this.title; };

var EBook = function(link) {
  Object.defineProperty(this, 'link', {
    writable: false,
    value: link
  });
};
EBook.prototype = Object.create(Book.prototype, {
  download: {
    writable: false,
    value: function() { console.log('Start...'); }
  }
});
// 一定要修正 constructor
EBook.prototype.constructor = EBook;

// testing
var jsorz = new EBook('https://zhuanlan.zhihu.com/ElemeFE');
console.log(jsorz instanceof Book);
console.log(jsorz instanceof EBook);
console.log(jsorz.constructor === EBook);
console.log(jsorz.hasOwnProperty('getTitle') === false);
console.log(Object.getPrototypeOf(jsorz) === EBook.prototype);
console.log(Object.getPrototypeOf(jsorz).constructor === EBook);

Примечание:Object.getPrototypeOfВозвращенный находится в [Рисунок 1]__proto__указывает на.

1.3 Наследование в ES6

В ES2015 естьclassСинтаксический сахар, сextends,super,staticТакие ключевые слова больше похожи на «классы» в строго типизированных языках.

class Book {
  constructor(props) {
    this._title = props.title;
  }
  get title() { return this._title; }
  static staticMethod() {}
  toString() { return `Book_${ this._title }`; }
}

class EBook extends Book {
  constructor(props) {
    super(props);
    this._link = props.link;
  }
  set link(val) { this._link = val; }
  toString() { return `Book_${ this._link }`; }
}

Приведенный выше синтаксис действительно ясен и прост, давайте посмотрим на код, скомпилированный в ES5~

var Book = function () {
  function Book(props) {
    _classCallCheck(this, Book);
    this._title = props.title;
  }
  _createClass(Book, [{
    key: "toString",
    // 省略...
  }, {
    key: "title",
    // 省略...
  }], [{
    key: "staticMethod",
    // 省略...
  }]);
  return Book;
}();

var EBook = function (_Book) {
  function EBook(props) {
    // 省略...
  }
  _inherits(EBook, _Book);
  _createClass(EBook, [{
    key: "toString",
    // 省略...
  }, {
    key: "link",
    // 省略...
  }]);
  return EBook;
}(Book);

Код, созданный в примере, можно использовать сBabel REPLПроверьте это, вы можете увидеть, что предоставляет ES6classСинтаксис действительно синтаксический сахар, по сути тот же «класс» и «наследство», мы издевались с ES5 и ранее.

1.4 Резюме

JavaScript легко имитировать «класс», и в определенной степени он может реализовать три характеристики объектной ориентации: инкапсуляцию, наследование и полиморфизм. От начальной симуляции «класса» до более удобного API для работы с прототипами, предоставляемого ES5, до предоставления большего количества ключевых слов, связанных с «классом», в ES6 — все это помогает нам снизить стоимость объектно-ориентированного использования в JavaScript.

Хотя «наследование» в JavaScript не является настоящим наследованием, а «класс» — не настоящим «классом», определенно есть много мест, которые невозможно реализовать по сравнению с Java, например абстрактный класс, интерфейс и т. д., только через некоторые хитрые методы моделирования. Таким образом, так называемое «наследование» в JavaScript призвано облегчить программистам организацию кода объектно-ориентированным способом.

2. Попробуйте множественное наследование

Жадность — это человеческая природа, после получения «наследства» мы захотим «множественного наследования». Даже во внутренних языках немногие языки действительно могут реализовать множественное наследование.Автор знает только, что C++ и python обеспечивают синтаксис множественного наследования, в то время как Java допускает наследование только одного родительского класса, но может одновременноimplementsМножественные классы интерфейса можно рассматривать как замаскированную форму множественного наследования.

2.1 Вопросы, которые следует учитывать при множественном наследовании

Множественное наследование не так хорошо, как предполагалось, в первую очередьinstanceofболее высокие требования

class A {}
class B {}
// 假定有支持多继承的语法
class C extends A, B {}
// 那么 C 的实例对象,应该同时也是 A 和 B 的 instance
let c = new C()
c instanceof C  // true
c instanceof A  // true
c instanceof B  // true

Как и в приведенном выше примере, при множественном наследовании все идентификаторы родительского класса должны быть записаны в подклассе, чтобыinstanceofдобиться вышеуказанного эффекта. Только в JavaScriptprototypeЦепочка, блин, тоже обязывает объект указывать только одинprototype, поэтому мы должны найти другой способ моделированияinstanceof

Это ничего, пожалуйста, смотрите следующую картинку

【Рисунок 2】Алмазная проблема

Это типичная проблема множественного наследования, называемая проблемой алмаза.Когда A, B и C определяют функцию с одним и тем же именем и когда функция вызывается в экземпляре объекта D, кого следует выполнять? . .

2.2 Косвенное множественное наследование

Во-первых, и во-вторых, мы заимствовали идеи из Java, фактически наследуя только один класс и интегрируя функции других классов другими способами. Доступно на JavaInterfaceОграничить поведение, которое должен иметь класс, конечно же, JavaScript может сделать то же самое, реализовать синтаксический сахар для интерфейса, проверить, переопределил ли «класс» все функции в интерфейсе. Но в этом случае интерфейс не имеет никакого практического значения, кроме проверки, и лучше идти прямо на пути миксина.

const mixinClass = (base, ...mixins) => {
  const mixinProps = (target, source) => {
    Object.getOwnPropertyNames(source).forEach(prop => {
      if (/^constructor$/.test(prop)) { return; }
      Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
    })
  };

  let Ctor;
  if (base && typeof base === 'function') {
    Ctor = class extends base {
      constructor(...props) {
        super(...props);
      }
    };
    mixins.forEach(source => {
      mixinProps(Ctor.prototype, source.prototype);
    });
  } else {
    Ctor = class {};
  }
  return Ctor;
};

class A {
  methodA() {}
}
class B {
  methodB() {}
}
class C extends mixinClass(A, B) {
  methodA() { console.log('methodA in C'); }
  methodC() {}
}

let c = new C();
c instanceof C  // true
c instanceof A  // true
c instanceof B  // false

Это просто имитирует косвенное множественное наследование.Создавая промежуточный класс, пусть промежуточный класс наследует A напрямую и смешивает прототипы членов B, а затем позволяет C унаследовать этот промежуточный класс. Поскольку B является неглубокой копией через миксин,B.prototypeне в цепочке прототипов C (C.__proto__.__proto__),такc instanceof Bявляется ложным.

исправлятьinstanceof, вы можете реализовать другой набор только самостоятельноisInstanceOf()Логика, запишите все ссылки на родительский класс во время наследования, а затем сравните.

2.3 Алгоритм ТОиР

Для второй проблемы, рассматриваемой для множественного наследования, упомянутой выше проблемы алмаза, необходимо ввести определение.

Порядок разрешения метода (MRO) относится к определению линейного порядка классов в структуре наследования, например.C => B => AЭто означает, что C наследует B, а B наследует A, тогда MRO C равенC B A, что означает, что при вызове функции в экземпляре CC B Aприоритетный порядок «нахождения» функции. В структуре с одинарным наследованием проблем, естественно, не возникает, тогда как при множественном наследовании MRO играет свою роль.

общийАлгоритм C3Он используется для расчета MRO, и его полное описание есть в документации по python.Вот пример, кратко описывающий поток алгоритма.

Предположим сейчас есть такая структура множественного наследования

[Рисунок 3] Пример структуры множественного наследования

Во-первых, вводится метод представления линейного порядка класса, как видно на приведенном выше рисунке.B => Y => OЭта часть представляет собой единую структуру наследования, очевидно, что MRO для B являетсяB Y O, обозначаемый какL(B) = BYO

Затем нужно ввести еще несколько символов в линейном порядке MRO: голова для первого элемента и хвост для остальных. Например,B Y OГолова в томB, хвост естьY O. В MRO есть только один элемент, как показано на [Рисунок 3].Oэлемент, головаO, хвост пустой.

Далее наиболее критично, MRO A на рисунке записывается какL(A(X, Y)),A(X, Y)означает, что A наследует и X, и Y, тогда

L(A(X, Y)) = A + merge(L(X), L(Y), XY)

Правила слияния следующие

取出第一个序列的 head
如果,该 head 不在其它序列的 tail 中
     则把这个 head 添加到结果中并从所有的序列中移除它
否则,用下一个序列的 head 重复上一步
直到所有序列中的所有元素都被移除(或者无法找到一个符合的head)

Наконец, давайте рассчитаем линейный порядок каждого класса на рисунке 3 ниже.

L(O) = O
L(X) = X + L(O) = XO
L(Y) = Y + L(O) = YO
L(A) = A + merge(L(X), L(Y), XY)
     = A + merge(XO, YO, XY)
     = AX + merge(O, YO, Y)
     = AXY + merge(O, O)
     = AXYO
L(B) = B + L(Y) = BYO
L(C) = C + merge(L(A), L(B), AB)
     = C + merge(AXYO, BYO, AB)
     = CA + merge(XYO, BYO, B)
     = CAX + merge(YO, BYO, B)
     = CAXB + merge(YO, YO)
     = CAXBYO

Пример python приведенной выше структуры множественного наследования можно найти по адресуИз-за отхаркивания.IO/snippets/hungry…Выводится ТОиР класса С, а именноC A X B Y O

Конечно, у алгоритма C3 есть и плохие случаи, которые приведут к сбою вышеописанного слияния в середине, то есть случай, когда невозможно получить MRO. Для получения более подробной информации о ТОиР см.The Python 2.3 Method Resolution OrderКороче говоря, не рекомендуется проектировать слишком сложную структуру множественного наследования =_=

2.4 Моделирование множественного наследования

На основе вышеизложенного давайте смоделируем реализацию множественного наследования:

  • Обеспечить независимый для каждого «класса»isInstanceOf()функция для решенияinstanceofЭта проблема
  • В то же время вводится алгоритм C3 порядка разрешения методов (MRO), а линейная последовательность MRO каждого «класса» хранится в метаданных.
  • Наследовать первый родительский класс в множественном наследовании, используя метод цепочки прототипов, и использовать метод примеси для остальных родительских классов.
const mixinProps = (target, source) => {
  Object.getOwnPropertyNames(source).forEach(prop => {
    if (/^(?:constructor|isInstanceOf)$/.test(prop)) { return; }
    Object.defineProperty(target, prop, Object.getOwnPropertyDescriptor(source, prop));
  })
};

const mroMerge = (list) => {
  if (!list || !list.length) {
    return [];
  }
  for (let items of list) {
    let item = items[0];
    let valid = true;
    for (let items2 of list) {
      if (items2.indexOf(item) > 0) {
        valid = false;
        break;
      }
    }

    if (valid) {
      let nextList = [];
      for (let items3 of list) {
        let _index = items3.indexOf(item);
        if (_index > -1) {
          items3.splice(_index, 1);
        }
        items3.length && nextList.push(items3);
      }
      return [item, ...mroMerge(nextList)];
    }
  }
  throw new Error('Unable to merge MRO');
};

const c3mro = (ctor, bases) => {
  if (!bases || !bases.length) {
    return [ctor];
  }
  let list = bases.map(b => b._meta.bases.slice());
  list = list.concat([bases]);
  let res = mroMerge(list);
  return [ctor, ...res];
};

const createClass = (parents, props) => {
  const isMulti = parents && Array.isArray(parents);
  const superCls = isMulti ? parents[0] : parents;
  const mixins = isMulti ? parents.slice(1) : [];

  const Ctor = function(...args) {
    // TODO: call each parent's constructor
    if (props.constructor) {
      props.constructor.apply(this, args);
    }
  };

  // save c3mro into _meta
  let bases = [superCls, ...mixins].filter(item => !!item);
  Ctor._meta = { bases: c3mro(Ctor, bases) };

  // inherit first parent through proto chain
  if (superCls && typeof superCls === 'function') {
    Ctor.prototype = Object.create(superCls.prototype);
    Ctor.prototype.constructor = Ctor;
  }

  // mix other parents into prototype according to [Method Resolution Order]
  // NOTE: Ctor._meta.bases[0] always stands for the Ctor itself
  if (Ctor._meta.bases.length > 1) {
    let providers = Ctor._meta.bases.slice(1).reverse();
    providers.forEach(provider => {
      // TODO: prototype of superCls is already inherited by __proto__ chain
      (provider !== superCls) && mixinProps(Ctor.prototype, provider.prototype);
    });
  }
  mixinProps(Ctor.prototype, props);

  Ctor.prototype.isInstanceOf = function(cls) {
    let bases = this.constructor._meta.bases;
    return bases.some(item => item === cls) || (this instanceof cls);
  }
  return Ctor;
};

Затем давайте протестируем структуру множественного наследования, как показано на [рис. 3].

const O = createClass(null, {});
const X = createClass([O], {});
const Y = createClass([O], {
  methodY() { return 'Y'; }
});
const A = createClass([X, Y], {
  testName() { return 'A'; }
});
const B = createClass([Y], {
  testName() { return 'B'; }
});
const C = createClass([A, B], {
  constructor() {
    this._name = 'custom C';
  }
});

let obj = new C();
console.log(obj.isInstanceOf(O)); // true
console.log(obj.isInstanceOf(X)); // true
console.log(obj.isInstanceOf(Y)); // true
console.log(obj.isInstanceOf(A)); // true
console.log(obj.isInstanceOf(B)); // true
console.log(obj.isInstanceOf(C)); // true
console.log(obj.testName());
console.log(obj.methodY());

Вышеприведенный код предназначен только для обучения, и все еще есть много недостатков, например, конструктор может вызывать только свой собственныйconstructorфункция, не может вызвать родительский классconstructor. Это связано с ограничениями JavaScript, которые нельзя передатьXX.prototype.constructor.apply()способ вызова конструкторов других классов (constructorтолько вnewназывается). Если вы хотите обойти эту проблему, вы можете изменить только имя функции, называемой Такие имена, как инициализация и инициализация, подходят.

демо-код здесь, попробуйте изменить параметры и попытайтесь понять упомянутый выше алгоритм C3 MRO.

2.5 Существующие проблемы

В приведенном выше коде для имитации множественного наследования только первый родительский класс помещается в цепочку прототипов подкласса, а другие родительские классы могут только копировать свойства в своем прототипе в прототип подкласса через миксин. Это ограничено механизмом цепочки прототипов JavaScript, т.е. [Рисунок 1].__proto__может указывать только на одну цель. Так как это реализовано таким образом, это должно противоречить реальному множественному наследованию, как в C++.таблица виртуальных функцийМеханизм при вызове функции множественного наследования будет искать в таблице реальный адрес функции. И мы смоделировали Множественное наследование JavaScript заключается в переносе всех функций родительского класса в прототип (Просто втирайте в порядке приоритета MRO).

Если вы внимательно посмотрите на приведенный выше код, вы обнаружите, чтоc.testName()Выходные данные не соответствуют алгоритму, описанному в Порядке разрешения метода. В этом разделе мы знаем, что MRO для C должен бытьC A X B Y O, в примере кода он должен вызываться первым в AtestName()函数,实际却输出了“B”……卧槽,这代码有毒的吧? ?

// inherit first parent through proto chain
if (superCls && typeof superCls === 'function') {
  Ctor.prototype = Object.create(superCls.prototype);
  Ctor.prototype.constructor = Ctor;
}
// mix other parents into prototype according to [Method Resolution Order]
// NOTE: Ctor._meta.bases[0] always stands for the Ctor itself
if (Ctor._meta.bases.length > 1) {
  let providers = Ctor._meta.bases.slice(1).reverse();
  providers.forEach(provider => {
    // TODO: prototype of superCls is already inherited by __proto__ chain
    (provider !== superCls) && mixinProps(Ctor.prototype, provider.prototype);
  });
}

Обратите внимание, что код в заявлении(provider !== superCls)filter, вы можете удалить его и попробовать демо. .

Тут автор тоже запутался, т.к.superClsЭто первый родительский класс, который был унаследован в цепочке прототипов.При смешивании других родительских классов в соответствии с порядком MRO первый родительский класс должен быть отфильтрован. Однако после добавления(provider !== superCls)После условия свойства прототипа других родительских классов копируются вCtor.prototype, а прототип в первом родительском классе находится в цепочке прототипов Ctor, очевидноCtor.prototypeВышеуказанные функции имеют более высокий приоритет.

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

В конечном счете, это все еще горшок с «поиском таблицы без функций»! Или мы вводим строгие ограничения на способ использования, все вызовы функций при множественном наследовании должны проходить через унифицированную форму, такую ​​какinvoke(methodName, args)интерфейс, при вызове, в соответствии с порядком приоритета MRO, проверить, есть лиmethodNameфункцию, а затем выполнить настоящий вызов.

3. Почему наследование не рекомендуется

Сказав так много, опыт автора таков, чтоНе думайте о наследстве,Не думайте о наследстве,Не думайте о наследстве. . .

JavaScript сам по себе не является объектно-ориентированным языком, зачем позволять ему делать то, в чем он не силен =_= Хотя синтаксический сахар уже обеспечивает поддержку «классов», то есть заботиться о людях с объектно-ориентированными идеями, но это принципиально иное Наследование в других языках. Не используйте терпимость других как причину невмешательства.Хорошо иметь возможность имитировать наследование и не беспокоиться о «множественном наследовании».

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

4. Резюме

Эта статья начинается с механизма языка JavaScript и рассматривает, как «классы» и «наследование» становятся все более и более удобными в JavaScript по мере развития языка. Затем обсуждаются проблемы, которые необходимо учитывать при «множественном наследовании», вводится порядок разрешения методов (MRO) и алгоритм C3, а также делается попытка смоделировать «множественное наследование» в JavaScript.

Однако в JavaScript нет концепции «классов» как таковой и не существует в истинном смысле наследования. Этот вид «множественного наследования», моделируемый прототипом, определенно не идеален, и опыт намного хуже, чем у языков, изначально поддерживающих наследование. Так что не думайте о множественном наследовании, и не рекомендуется часто использовать наследование в JavaScript.

использованная литература

You Don't Know JS: this & Object Prototypes

Java Doc: Polymorphism

The Python 2.3 Method Resolution Order

C3 linearization

dojo class declaration