• 首页
  • vue
  • TypeScript
  • JavaScript
  • scss
  • css3
  • html5
  • php
  • MySQL
  • redis
  • jQuery
  • 混入类型(Mixins)

    你可能在 Scala 等语言里对mixinstrait已经很熟悉了,这种模式在 JavaScript 中也是很流行的。

    在 TypeScript 中,可以根据不同的功能定义多个可复用的类,然后组合(或者扩展)这些可复用的类,从而快速搭建起一个功能强大的类。这就是mixins模式。

    组合(或者扩展)可复用类,常借助extendsimplementsextends只支持继承一个父类,implements可以连接多个可复用的类,并且使用原型链连接子类的方法和父类的方法。

    Mixin 如何工作?

    该模式依赖于使用具有类继承的泛型扩展基类。TypeScript 最好的 mixin 支持是通过类表达式模式实现的。

    开始,我们需要一个基类,在基类的基础上应用 mixin:

    class Sprite {
      name = "";
      x = 0;
      y = 0;
     
      constructor(name: string) {
        this.name = name;
      }
    }
    

    然后需要一个类型(type)和一个工厂函数(factory function),它返回一个扩展基类的类表达式

    // To get started, we need a type which we'll use to extend
    // other classes from. The main responsibility is to declare
    // that the type being passed in is a class.
     
    type Constructor = new (...args: any[]) => {};
     
    // This mixin adds a scale property, with getters and setters
    // for changing it with an encapsulated private property:
     
    function Scale<TBase extends Constructor>(Base: TBase) {
      return class Scaling extends Base {
        // Mixins may not declare private/protected properties
        // however, you can use ES2020 private fields
        _scale = 1;
     
        setScale(scale: number) {
          this._scale = scale;
        }
     
        get scale(): number {
          return this._scale;
        }
      };
    }
    

    设置好这些之后,您可以创建一个类,该类表示应用 mixin 的基类:

    // Compose a new class from the Sprite class, with the Mixin Scale applier:
    const EightBitSprite = Scale(Sprite);
     
    const flappySprite = new EightBitSprite("Bird");
    flappySprite.setScale(0.8);
    console.log(flappySprite.scale);
    


    受约束的混入

    在上面的表单中,mixin 没有类的基础知识,这会使您很难创建所需的设计。为了对此建模,我们修改原始构造函数类型以接受泛型参数。

    // This was our previous constructor:
    type Constructor = new (...args: any[]) => {};
    // Now we use a generic version which can apply a constraint on
    // the class which this mixin is applied to
    type GConstructor<T = {}> = new (...args: any[]) => T;
    

    这允许创建仅使用受约束基类的类:

    type Positionable = GConstructor<{ setPos: (x: number, y: number) => void }>;
    type Spritable = GConstructor<Sprite>;
    type Loggable = GConstructor<{ print: () => void }>;
    

    然后,您可以创建 mixin,只有当您有一个特定的基础来构建时,它才能工作:

    function Jumpable<TBase extends Positionable>(Base: TBase) {
      return class Jumpable extends Base {
        jump() {
          // This mixin will only work if it is passed a base
          // class which has setPos defined because of the
          // Positionable constraint.
          this.setPos(0, 20);
        }
      };
    }
    


    备选模式

    本文档的早期版本推荐了一种编写 mixin 的方法,您可以分别创建运行时和类型层次结构,然后在最后合并它们:

    // Each mixin is a traditional ES class
    class Jumpable {
      jump() {}
    }
     
    class Duckable {
      duck() {}
    }
     
    // Including the base
    class Sprite {
      x = 0;
      y = 0;
    }
     
    // Then you create an interface which merges
    // the expected mixins with the same name as your base
    interface Sprite extends Jumpable, Duckable {}
    // Apply the mixins into the base class via
    // the JS at runtime
    applyMixins(Sprite, [Jumpable, Duckable]);
     
    let player = new Sprite();
    player.jump();
    console.log(player.x, player.y);
     
    // This can live anywhere in your codebase:
    function applyMixins(derivedCtor: any, constructors: any[]) {
      constructors.forEach((baseCtor) => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
          Object.defineProperty(
            derivedCtor.prototype,
            name,
            Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
              Object.create(null)
          );
        });
      });
    }
    

    此模式较少依赖编译器,更多依赖代码库,以确保运行时和类型系统正确保持同步。


    约束条件

    通过代码流分析,在 TypeScript 编译器内部本机支持 mixin 模式。在一些情况下,您可以触及本机支持的边缘。

    装饰器和混入

    您不能使用修饰符通过代码流分析提供 mixin:/p>

    // A decorator function which replicates the mixin pattern:
    const Pausable = (target: typeof Player) => {
      return class Pausable extends target {
        shouldFreeze = false;
      };
    };
     
    @Pausable
    class Player {
      x = 0;
      y = 0;
    }
     
    // The Player class does not have the decorator's type merged:
    const player = new Player();
    player.shouldFreeze;
    Property 'shouldFreeze' does not exist on type 'Player'.
     
    // The runtime aspect could be manually replicated via
    // type composition or interface merging.
    type FreezablePlayer = Player & { shouldFreeze: boolean };
     
    const playerTwo = (new Player() as unknown) as FreezablePlayer;
    playerTwo.shouldFreeze;
    


    静态属性混入

    与其说是约束,不如说是抓住了。类表达式模式创建单例,因此不能在类型系统中映射它们以支持不同的变量类型。

    您可以通过使用函数返回基于泛型的不同类来解决此问题:

    function base<T>() {
      class Base {
        static prop: T;
      }
      return Base;
    }
     
    function derived<T>() {
      class Derived extends base<T>() {
        static anotherProp: T;
      }
      return Derived;
    }
     
    class Spec extends derived<string>() {}
     
    Spec.prop; // string
    Spec.anotherProp; // string
    


    对象混入

    使用 es6 的Object.assign合并多个对象。

    interface Name {
        name: string
    }
    interface Age {
        age: number
    }
    interface Sex {
        sex: number
    }
     
    let people1: Name = { name: "张三" }
    let people2: Age = { age: 20 }
    let people3: Sex = { sex: 1 }
     
    const people = Object.assign(people1,people2,people3)
    //people 会被推断成一个交差类型 Name & Age & sex;
    


    类的混入

    首先声明两个 mixins 类(严格模式要关闭不然编译不过)。使用implements,把类当成了接口。我们可以这么做来达到目的,为将要 mixin 进来的属性方法创建出占位属性。这告诉编译器这些成员在运行时是可用的。

    class A {
        type: boolean = false;
        changeType() {
            this.type = !this.type
        }
    }
     
     
    class B {
        name: string = '张三';
        getName(): string {
            return this.name;
        }
    }
    
    class C implements A,B{
        type:boolean
        changeType:()=>void;
        name: string;
        getName:()=> string
    }
    

    最后,创建这个帮助函数,帮我们做混入操作。它会遍历 mixins 上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。

    Object.getOwnPropertyNames()可以获取对象自身的属性,除去他继承来的属性,对它所有的属性遍历,它是一个数组,遍历一下它所有的属性名。

    Mixins(C, [A, B])
    function Mixins(curCls: any, itemCls: any[]) {
        itemCls.forEach(item => {
            Object.getOwnPropertyNames(item.prototype).forEach(name => {
                curCls.prototype[name] = item.prototype[name]
            })
        })
    }
    
    let c = new C()