For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/proposal-mixins.md.
  • 简体中文
  • Maximally minimal mixins S1

    中文标题:最简最小化混入

    提案概览
    提案速览

    该提案为JavaScript中的混入引入声明式语法,旨在将现有的子类工厂模式正式化。它定义了用于声明的mixin关键字和用于应用的with关键字,支持静态分析和改善人体工学。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    最简最小化混入

    @justinfagnani

    最后更新:2018-01-26

    状态:Stage 1

    查看 TC39 Slides

    与一等协议提案的重叠

    本提案和一等协议提案在它们所覆盖的问题和解决方案空间上有部分重叠。在2018年1月的TC39会议上,同意将这两者都保持在Stage 1,并打算一起努力确定它们针对的常见问题,并可能形成统一的提案。

    目标

    • 用声明式语法铺设子类工厂模式现有的牛路
    • 支持对混入和混入应用的静态分析
    • 使混入继承比现在更具吸引力,成为子类继承的易用替代方案
    • 与类正交 - 类特性自然适用于混入
    • 为协议/特征类特性的发展提供空间
    • 避免makeMethod、非线性原型链等问题

    子类工厂模式

    JavaScript可以说已经具有混入,这得益于类表达式和extends表达式。

    let M = (base) => class extends base {
      field = 'abc';
    
      constructor() {
        super();
        // ...
      }
    
      method() {
        super.method();
        // ...
      }
    }
    class S {}
    class C extends M(S) {}

    该模式的细节和好处在这篇2015年的博客文章中有所解释:http://justinfagnani.com/2015/12/21/real-mixins-with-javascript-classes/

    子类工厂的语法糖

    最简最小化混入是一种简单语法,将以下内容脱糖为上面的代码:

    mixin M {
      field = 'abc';
    
      constructor() {
        super();
        // ...
      }
    
      method() {
        super.method();
        // ...
      }
    }
    class S {}
    class C extends S with M {}

    这些混入在功能上严格弱于命令式子类工厂。例如,没有办法以命令方式修改子类的原型,也没有办法进行混入组合等。这些能力保留给未来的演进。

    为什么?

    如果声明式混入只是已有模式的弱化形式的语法糖,那么明显的问题是:为什么要添加它们?

    启用静态分析

    声明式语法最直接的好处可能是提高了工具化程度。当前的混入模式非常命令式,静态分析很难识别和正确理解该模式。

    JavaScript的类型化变体,例如Closure和TypeScript,最近已经添加了对命令式模式的支持,但在两者中都极为脆弱和麻烦。有关代码需要如何构造以满足各种工具的示例,请参见下面的附录。

    改善人体工学

    即使在类型检查之外,声明式混入的人体工学也比命令式子类工厂模式更好。

    鼓励兼容类的混入

    子类工厂混入利用了类,并以其他混入模式没有的方式与语言集成。由于子类工厂混入使用实际的类声明/表达式,它们自动获得类的所有新特性,如公共和私有字段以及装饰器。

    针对该模式的特定语法将鼓励其使用,并且一旦它们成为语言的官方部分,可能允许平台首次提供混入。

    例如,DOM标准最近添加了一个EventTarget基类,可用于继承Web平台的addEventListener()和相关API。这很好,但有通常的通过子类继承共享代码的问题。通过语言级别的混入,DOM有更好的机会添加EventTargetMixin

    启用未来改进

    就像最简最小化类一样,最简最小化混入为进一步的演进奠定了基础。该领域的其他提案(混入、特征、接口和协议)包括了额外功能,如需求、命名空间、别名等。这些功能可以添加到类和混入中,但一个简单的起点似乎会有所帮助。

    细节

    混入声明

    _混入_概念上是一个抽象子类 - 在声明时没有已知超类的类。为了与当前JavaScript语言特性最大兼容,我们将混入定义为子类工厂 - 一个从类到新的子类的函数。声明定义了超类和生成的子类之间的差异。

    使用新的mixin关键字声明混入:

    mixin Name {
    }

    与类一样,mixin可以用于声明或表达式。混入声明或表达式在语法上类似于类声明或表达式。它有一个可选的BindingIdentifier和一个ClassBody。

    混入定义了一个箭头函数(因此它们不能是构造函数),该函数有一个参数 superclass。混入返回参数的新子类,就好像混入体是作为类表达式求值,并使用参数的值作为 superclass,而不是像ClassDefinitionEvaluation运行时语义那样计算ClassHeritage。

    混入应用

    通过使用with关键字将混入 应用 到一个具体超类来使用混入。(这可能与with语句混淆,因此可以使用另一个关键字)

    A with M调用混入函数M,参数为A。这可以用于类声明的extends子句中:

    class C extends A with M {}

    在一个类声明中应用多个混入会很常见,因此我们可以为混入列表提供特殊语法:

    class C extends A with M1, M2, M3 {}

    Symbol.mixin

    constructor属性类似,应该能够识别用于创建原型的混入。constructor本身不适合标识 混入,因为它将引用由混入应用生成的构造函数。混入应用将把构造函数的prototypeSymbol.mixin属性设置为引用混入函数。

    mixin M {}
    class C extends A with M {}
    Object.getPrototypeOf(C).prototype.hasOwnProperty(Symbol.mixin); // true
    Object.getPrototypeOf(C).prototype[Symbol.mixin] === M; // true

    instanceof

    可以定义instanceof使其与混入一起工作:

    mixin M {}
    class C extends Object with M {}
    new C() instanceof M; // true

    混入组合

    由于混入只是函数,它们自然可以组合函数语法:

    mixin A {}
    mixin B {}
    let AB = (superclass) => A(B(superclass));

    组合通常希望与声明内联。我们可以允许混入像类一样有extends子句,但必须求值为混入:

    mixin A extends B {}

    脱糖

    混入声明
    mixin M {
      #a;
      b;
      c();
    }

    脱糖为:

    Symbol.mixin = Symbol.mixin || Symbol('mixin');
    
    let M = (superclass) => {
      const _M = class extends superclass {
        #a;
        b;
        c() {}
      }
      Object.defineProperty(_M.prototype, Symbol.mixin, {value: M});
      return _M;
    };
    
    Object.defineProperty(M, 'prototype', {});
    
    Object.defineProperty(M, Symbol.hasInstance, {
      value(o) {
        while (o !== undefined) {
          if (o.hasOwnProperty(Symbol.mixin) && o[Symbol.mixin] === this) {
            return true;
          }
          o = Object.getPrototypeOf(o);
        }
        return false;
      }
    });
    混入应用
    C with M;

    脱糖为:

    M(C)

    因此:

    class A extends B with M {
      // ...
    }

    脱糖为:

    class A extends M(B) {
      // ...
    }
    混入组合
    mixin A extends B {
      method() {}
    }

    脱糖为:

    let A = (superclass) => class extends B(superclass) {
      method() {}
    }

    有趣的说明

    与类的正交性

    此提案的主要含义之一是混入与类大致正交。混入是一种声明抽象子类并利用类语法和语义的方式。类的所有新声明式特性都会自动被混入继承。

    这种正交性可以用于类和混入的演进。之前讨论和/或提议的功能的某些部分可以分解出来,并独立于混入添加到类中。

    混入方法在应用时被复制

    听起来以前关于共享原型/方法的讨论不得不处理函数身份和home objects的问题。该提案通过每次混入应用时通过类表达式求值来复制方法,回避了这些问题。

    这意味着混入中定义的方法每次混入应用都会获得新的副本:

    mixin M {
      method() {}
    }
    
    class A extends Object with M {}
    class B extends Object with M {}
    
    A.prototype.method === B.prototype.method; // false

    混入是函数,但不是构造函数

    混入在语法上可能看起来像类,用户可能期望它们有.prototype属性,但它们没有。能够访问混入的原型会很有用,但在本提案中没有简单的方法允许这样 - 原型在应用之前不存在。它实际上是一个 潜在 的原型。

    为了为未来在混入声明时存在原型对象的功能保留空间,我们可以将混入的prototype属性定义为undefined,不可写,不可配置。这在上面的混入声明脱糖中已显示。

    演进

    最简最小化混入旨在允许演进,就像最简最小化类成功的那样,包括从许多先前提出的混入或相关功能中添加特性。

    一等协议

    参见:https://github.com/michaelficarra/proposal-first-class-protocols

    一等协议提案为类/继承添加了一些有趣的特性:

    • 接口
    • 需求
    • 类似混入的组合
    • 自动命名空间成员

    需求可以添加到类和混入中(见下面的特征)。

    自动命名空间成员(一次性声明一个Symbol和由该Symbol命名的成员的成员)似乎独立于协议通用有用。如果可以将它们添加到类,那么混入自然也会获得它们。

    这可以用辅助函数今天实现,以后可能会添加语法:

    let makeSymbol = (o, name) => o[name] = Symbol(name);
    class A {
      [makeSymbol(A, 'myMethod')]() { /* ... */ }
    }
    class B extends A {
      [A.myMethod]() {
        super[A.myMethod]();
        // ...
      }
    }

    特征

    参见:http://zqsmm.qiniucdn.com/data/20110512092812/index.html#Traits

    需求

    特征可以要求类实现某些成员:

    trait ComparableTrait {
      requires lessThan;
      requires equals;
    }

    最简最小化混入为在类上添加这些功能留有余地,从而也适用于混入。

    大致上,类成员上的requires类修饰符可能会与抽象类的某种概念相关联。在声明具体子类时将检查需求。具有需求的混入在应用到具体类时会检查需求。

    abstract class A {
      require x;
    }
    abstract class B extends A {} // OK
    class C extends A {} // Error: Must implement foo;
    class D extends A { // OK
      foo() {}
    }
    别名

    待定:声明式别名/重命名需要一些语法来描述重命名...

    附录:子类工厂分析

    TypeScript

    TypeScript在2.2中添加了对混入的支持。它将混入建模为子类工厂的参数和工厂中定义的类的交集。这并不完全准确,因为它没有建模覆盖语义,但通常足够接近。

    export type Constructor<T=object> = new(...args: any[]) => T;
    
    class S {
      baseMethod() {/*...*/}
    }
    
    let M = <T extends Constructor>(base: T) => class extends base {
      constructor(...args: any[]) {
        super(...args);
      }
    
      mixinMethod() {/*...*/}
    };
    
    class C extends M(Object) {
      subclassMethod() {/*...*/}
    }
    
    let c = new C();

    TypeScript正确推断c具有从SMC继承的成员。

    在TypeScript中,类在值和类型命名空间中定义名称,但函数不会,因此M是一个值,而不是一个类型。如果要将M用作类型,你必须用接口重复声明,并将新类转换为接口:

    interface M {
      mixinMethod();
    }
    
    let M = <T extends Constructor>(base: T) => class extends base {
      constructor(...args: any[]) {
        super(...args);
      }
    
      mixinMethod() {/*...*/}
    } as T & Constructor<M>;

    这显然随着混入接口变大而变得更加麻烦。

    Closure

    Closure需要更多样板代码来描述混入并应用到编译器:

    class S {
      baseMethod() {/*...*/}
    }
    
    let M = (base) => {
      /*
       * @implements {MInterface}
       */
      class M extends base {
        constructor(...args: any[]) {
          super(...args);
        }
    
        mixinMethod() {/*...*/}
      }
      return M;
    };
    
    /**
     * 在Closure中,接口声明是必需的。
     * @interface
     */
    function MInterface(){}
    
    /** @return {void} */
    MInterface.prototype.mixinMethod = function(){};
    
    /**
     * Closure要求你单独定义并转换混入应用。
     * @constructor
     * @extends S
     * @implements {MInterface}
     */
    const CBase = M(Base);
    
    class C extends CBase {
      subclassMethod() {/*...*/}
    }
    
    let c = new C();
    Polymer Analyzer

    Polymer Analyzer通过自定义JSDoc注释具有对分析混入的特定支持:

    class S {
      baseMethod() {/*...*/}
    }
    
    /**
     * @mixinFunction 
     */
    let M = (base) => {
      /**
       * @mixinClass
       */
      class M extends base {
        constructor(...args: any[]) {
          super(...args);
        }
    
        mixinMethod() {/*...*/}
      }
      return M;
    };
    
    /**
     * @extends Object
     * @appliesMixin M
     */
    class C extends M(Object) {
      subclassMethod() {/*...*/}
    }