[JavaScript翻译]真正的 "混合器 "与JavaScript类

182 阅读12分钟

本文由 简悦SimpRead 转码,原文地址 justinfagnani.com

如何为JavaScript和ES6类建立一个简单、强大、真实的混合器系统。

混合器和Javascript。好的,坏的,和丑的。

混合器和JavaScript就像克林特-伊斯特伍德的经典电影。

好的方面是,由于JavaScript的灵活特性,用小块的实现来组成对象甚至是可能的,而且混合器在某些圈子里相当流行。

坏的方面有一长串:在JavaScript中甚至没有关于mixin概念的共同概念;没有关于它们的共同模式;它们需要辅助库来使用;更高级的组合(比如mixin和原型之间的协调)是复杂的,而且不会自然地从模式中掉出来;它们很难进行静态分析和内省;最后,大多数mixin库会突变对象或其原型,给VM和一些程序员带来问题,使他们无法避免。

丑陋的是,所有这些的结果是一个混合库和混合器的巴尔干化生态系统,各库的构造和语义往往不兼容。就我所知,没有一个特定的库流行到足以被称为通用库。你更有可能看到一个项目实现自己的混合函数,而不是看到一个混合库的使用。

到目前为止,JavaScript的混合函数根本就没有发挥出它们的潜力,没有达到学术文献中对混合函数的描述,甚至没有在一些好的语言中实现。

对于一个喜欢混合器,并且认为应该尽可能多地使用混合器的人来说,这太可怕了。混合器解决了单继承语言所存在的一些问题,对我来说,原型人群对JavaScript中的类的抱怨也是最多的。我认为所有的继承都应该是混合继承--子类只是混合应用的一种退化形式。

幸运的是,在隧道的尽头出现了JavaScript类的曙光。它们的到来终于给了JavaScript非常容易使用的继承语法。JavaScript类比大多数人意识到的要强大得多,而且是建立真正的混合器的一个很好的基础。

在这篇文章中,我将探讨混合器应该做什么,目前的JavaScript混合器有什么问题,以及在JavaScript中建立一个非常有能力的混合器系统是多么简单,它可以与类发挥得非常好。

到底什么是混合器?

为了理解混合器的实现应该做什么,让我们首先看看什么是混合器。

混合器是一个抽象的子类;也就是说,一个子类的定义可以应用于不同的超类,以创建一个相关的修改类家族。

Gilad Bracha和William Cook,基于混合素的继承

这是我所能找到的关于混合体的最好定义。它清楚地显示了混合器和普通类之间的区别,并强烈暗示了混合器如何在JavaScript中实现。

为了更深入地挖掘这个定义的含义,让我们在混合器词典中添加两个术语。

  • mixin definition。一个类的定义,可以应用于不同的超类。
  • mixin application: 将混合定义应用于一个特定的超类,产生一个新的子类。

mixin定义实际上是一个_子类工厂_,由超类提供参数,产生mixin应用。混合器应用位于子类和超类之间的继承层次中。

mixin和普通子类之间真正的,也是唯一的区别是,普通子类有一个固定的超类,而mixin定义还没有一个超类。只有_mixin应用程序_有自己的超类。你甚至可以把普通子类的继承看成是mixin继承的一种退化形式,在类定义时就知道超类,而且只有一个应用。

例子

下面是一个Dart中的mixins的例子,它有一个很好的mixins语法,同时与JavaScript相似。

class B extends A with M {}

这里 A 是一个基类,B 是子类,而 M 是一个混合器。mixin的应用是 M 混入 A 的具体组合,通常称为 A-with-MA-with-M 的超类是 A ,而 B 的实际超类不是 A ,正如你所期望的,而是 A-with-M

一些类的声明和图表可能有助于说明发生了什么。

让我们从一个简单的类层次结构开始,类 B 继承于类 A

class B extends A {}

现在我们来添加混合器。

Class B extends A with M {}

正如你所看到的,混合应用_A-with-M_被插入了子类和超类之间的层次结构中。

注意:我用长虚线表示混合体的声明(B_包括_M),用短虚线表示混合体应用的定义。

多重混合器

在Dart中,多个混合器按从左到右的顺序应用,导致多个混合器应用被添加到继承层次中。

class B extends A with M1, M2 {}

class C extends A with M1, M2 {}

传统的JavaScript混合器

在JavaScript中自由修改对象的能力意味着可以非常容易地复制函数来实现代码重用,而不需要依赖继承。

Cocktailtraits.js这样的混合库,以及许多博客文章中描述的模式(比如最近在Hacker News上的一篇。Using ES7 Decorators as Mixins),一般都是通过修改对象,从混合器对象中复制属性并覆盖现有属性来实现的。

这通常是通过一个类似于这样的函数来实现的。

function mixin(source, target) {
  for (var prop in source) {
    if (source.hasOwnProperty(prop)) {
      target[prop] = source[prop];
    }
  }
}

这种方法的一个版本甚至已经进入了JavaScript,成为Object.assign

mixin()通常在原型上被调用。

mixin(MyMixin, MyClass.prototype)。


现在MyClass拥有MyMixin中定义的所有属性。

这有什么不好?

简单地将属性复制到目标对象中有一些问题。有些问题可以通过足够聪明的mixin函数来解决,但常见的例子通常有这些问题。

原型被就地修改。

当对原型对象使用mixin库时,原型会直接被修改。如果原型被用在其他不需要混入属性的地方,这就是一个问题。添加状态的混入库会在虚拟机中产生较慢的对象,这些虚拟机试图在分配时理解对象的形状。这也违背了混入应用应该通过合成现有的类来创建一个新的类的想法。

super不起作用。

随着JavaScript最终支持super,mixin也应该支持。一个mixin的方法应该能够委托给原型链上的重写方法。由于super本质上是词法上的约束,这在复制函数中是行不通的。

优先级不正确。

这不一定总是这样,但正如例子中经常显示的,通过覆盖属性,mixin方法优先于子类中的方法。它们应该只优先于超类中的方法,允许子类覆盖混合器上的方法。

构成受到影响

混合体经常需要委托给其他混合体或原型链上的对象,但在传统的混合体中没有自然的方法可以做到这一点。由于函数被复制到对象上,天真的实现方式会覆盖现有的方法。更复杂的库会记住现有的方法,并调用同名的多个方法,但库必须发明自己的组合规则。方法被调用的顺序是什么,传递什么参数,等等。

对函数的引用在mixin的所有应用中都是重复的,在很多情况下,它们可以被捆绑在一个共享原型中。通过覆盖属性,protytpes的结构和JavaScript的一些动态特性被削弱了:你不能轻易地反省mixin或者删除或重新排序mixin,因为mixin已经被直接扩展到目标对象中。

如果我们真正使用原型链,所有这些都会消失,只需很少的工作。

通过类表达式实现更好的混合器

现在让我们来看看好东西。Awesome Mixins™ 2015版

让我们快速列出我们想要启用的功能,这样我们就可以根据这些功能来判断我们的实现。

  1. 混合素被添加到原型链中。
  2. 混合器的应用无需修改现有对象。
  3. 混合函数不做任何魔术,也不在核心语言之上定义新的语义。
  4. "super.foo "属性的访问在混合体和子类中起作用。
  5. super()调用在构造函数中起作用。
  6. 混合器能够扩展其他混合器。
  7. `instanceof'可以工作。
  8. 混合器的定义不需要库的支持--它们可以用通用的风格来写。

子类因素与这个奇怪的技巧

上面我把mixin称为 "由超类提供参数的子类工厂",在这个表述中,它们实际上就是这样。

我们依赖于JavaScript类的两个特性。

  1. class可以作为一个表达式和一个语句使用。作为一个表达式,每次评估都会返回一个新的类。(有点像一个工厂!)
    1. extends子句可以接受任意的表达式来返回类或构造函数。

关键是,JavaScript中的类是第一类:它们是可以传递给函数和从函数中返回的值。

我们所需要定义的mixin是一个接受超类并从中创建一个新的子类的函数,像这样。

let MyMixin = (superclass) => class extends superclass {
  foo() {
    console.log('foo from MyMixin');
  }
};

然后我们可以在 "extends "子句中使用它,像这样。

class MyClass extends MyMixin(MyBaseClass) {
  /* ... */
}

MyClass现在通过mixin的继承有了一个foo方法。

let c = new MyClass();
c.foo(); // prints "foo from MyMixin"

令人难以置信的简单,也是令人难以置信的强大! 通过结合函数应用和类表达式,我们得到了一个完整的混合器解决方案,而且还能很好地泛化。

应用多个混合器的工作原理与预期相同。

class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
  /* ... */
}

混合器可以通过传递超类而轻松地继承其他混合器。

let Mixin2 = (superclass) => class extends Mixin1(superclass) {
  /* Add or override methods here */
}

而且你可以使用普通的函数组合来组成混合器。

let CompoundMixin = (superclass) => Mixin2(Mixin3(superclass));

混合器作为子类工厂的好处

这种方法给了我们一个很好的混合器的实现

子类可以重写mixin方法。

正如我之前提到的,很多JavaScript混合器的例子都搞错了,混合器覆盖了子类。用我们的方法,子类可以正确地覆盖混合器方法,而混合器方法则覆盖超类方法。

super工作。

最大的好处之一是,super在子类和混合器的方法中起作用。因为我们从来没有覆盖过类或混和物的方法,所以它们可以被super解决。

super的调用对于那些刚接触混搭的人来说可能有点不直观,因为在混搭定义时并不知道超类,有时开发者希望super能指向声明的超类(混搭的参数),而不是混搭的应用。对最终原型链的思考在这里有帮助。

组成被保留了。

这实际上只是其他好处的一个结果,但两个混 合体可以定义相同的方法,只要它们调用 super,它们都将被调用然后应用。

有时候,混合器不知道超类是否有特定的属性或方法,所以最好对超级调用进行保护。

let Mixin1 = (superclass) => class extends superclass {
  foo() {
    console.log('foo from Mixin1');
    if (super.foo) super.foo();
  }
};

let Mixin2 = (superclass) => class extends superclass {
  foo() {
    console.log('foo from Mixin2');
    if (super.foo) super.foo();
  }
};

class S {
  foo() {
    console.log('foo from S');
  }
}

class C extends Mixin1(Mixin2(S)) {
  foo() {
    console.log('foo from C');
    super.foo();
  }
}

new C().foo();

打印出来。

foo from C
foo from Mixin1
foo from Mixin2
foo from S

改进语法

我发现将mixin作为函数来应用既优雅又简单--准确地描述了正在发生的事情,但同时也有点丑陋。我最担心的是,这种结构没有为不熟悉这种模式的读者进行优化。

我希望能有一个更容易看懂的语法,至少能给新读者一些可以搜索的东西来解释发生了什么,就像Dart的语法。我还想增加一些功能,比如备忘录化mixin应用和自动实现instanceof支持。

为此,我们可以写一个简单的助手,将混合器的列表应用于超类,在一个类似流畅的API中。

class MyClass extends mix(MyBaseClass).with(Mixin1, Mixin2) {
  /* ... */
}

下面是代码。

let mix = (superclass) => new MixinBuilder(superclass);

class MixinBuilder {
  constructor(superclass) {
    this.superclass = superclass;
  }

  with(...mixins) { 
    return mixins.reduce((c, mixin) => mixin(c), this.superclass);
  }
}

就这样吧! 对于它所实现的功能和漂亮的语法来说,它还是非常简单的。

构造函数和初始化

构造函数是混合函数的一个潜在混淆源。它们的行为本质上与方法类似,只是被重写的方法往往有相同的签名,而继承层次中的构造函数往往有不同的签名。

由于mixin可能不知道它被应用于哪个超类,因此也不知道它的超级构造函数的签名,调用super()可能会很麻烦。处理这个问题的最好方法是始终将所有构造函数参数传递给super(),要么根本不定义构造函数,要么使用传播操作符:super(...arguments)

这意味着将参数专门传递给mixin的构造函数很困难。一个简单的解决方法是,如果mixin需要参数,就在其上设置一个明确的初始化方法。

进一步探索

这只是与JavaScript中混合器相关的许多主题的开始。我还会发表更多的内容,比如。

  • 用装饰函数增强混合器新的帖子,包括。
  • 缓存混合器的应用,使应用于同一超类的同一混合器能够重复使用一个原型。
  • instanceof发挥作用。
  • mixin继承如何解决人们对ES6类和经典继承对JavaScript不利的担心。
  • 在ES5中使用subclass-factory-style mixins。
  • 去除混合器的重复性,使混合器的组成更有用处。

mixwith.js项目

我在GitHub上启动了一个项目,围绕这个模式建立一个库。github.com/justinfagna… 它可以在npm上找到。www.npmjs.com/package/mix…

请关注我们的对话


www.deepl.com 翻译