首页 / TypeScript 入门教程 / Mixin 混入

TypeScript 入门教程

Mixin 混入

本教程共 80 篇 · 第 37 篇 · 更新于 2026-08-10 · 约 12 分钟阅读

TypeScriptTypeScript 入门教程Mixin混入组合

本节目标:掌握 TypeScript 的 Mixin 混入模式,学会用类表达式 + 泛型约束将多个独立的行为组合到一个类中,突破单继承的限制。

JavaScript 和 TypeScript 只支持单继承。一个类只能 extends 一个父类。但在实际开发中,经常有这种需求:一个类既需要”能被序列化”,又需要”能记录日志”,还需要”能克隆自己”。这些能力彼此独立,你没法把它们都塞进一个父类里——因为单继承塞不下。

Mixin 就是解决这个问题的。

问题场景

假设你在写一个游戏引擎,所有的游戏对象(角色、道具、场景)都有位置信息。但有的对象需要”能被缩放”,有的需要”能跳”,有的需要”能记录日志”。

用继承的话,层级会变成这样:

         GameObject
        /    |    \
   Scaled  Jumpable  Loggable
   /   \    /   \    /   \
  ...  ...  ...  ...  ...  ...

每多一种能力,就要多一层继承,组合爆炸。这就是”菱形继承”问题的变种——虽然 TypeScript 没有多继承,但需求的组合维度很多。

Mixin 的核心思路

Mixin 的思路是:不搞继承树,而是把每个能力写成一个函数,这个函数接收一个类,返回一个扩展后的新类。

// 第一步:定义一个通用的构造函数类型
type Constructor<T = {}> = new (...args: any[]) => T;

// 第二步:每个 Mixin 是一个函数,接收基类,返回扩展类
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();

    getAge(): number {
      return Date.now() - this.createdAt.getTime();
    }
  };
}

// 第三步:基类
class User {
  constructor(public name: string) {}
}

// 第四步:混入
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");

console.log(user.name);           // "Alice" —— 来自 User
console.log(user.createdAt);      // 当前时间 —— 来自 Mixin
console.log(user.getAge() < 100); // true —— 刚创建,时间很短

整个过程不修改 User 类本身,而是创建了一个新的类 TimestampedUser。这很关键——原始的 User 不受影响,你可以在其他地方继续用纯净的 User

解剖 Constructor 类型

type Constructor<T = {}> = new (...args: any[]) => T;

这行代码看着有点吓人,拆开看:

  • new (...args: any[]):表示”一个构造函数,接受任意参数”。
  • => T:这个构造函数返回的类型是 T
  • <T = {}>:泛型参数,默认值是 {},表示”不设任何约束”。

说白了,Constructor<T> 的意思是”任意一个能 new 的类,它的实例满足 T 类型”。这个类型是整个 Mixin 模式的基础。

组合多个 Mixin

Mixin 的真正威力在于可以叠加。你可以把多个 Mixin 像搭积木一样串起来:

type Constructor<T = {}> = new (...args: any[]) => T;

// Mixin 1:添加时间戳
function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    createdAt = new Date();
  };
}

// Mixin 2:添加序列化
function Serializable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    serialize(): string {
      return JSON.stringify(this);
    }
  };
}

// Mixin 3:添加日志
function Loggable<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    log(message: string): void {
      console.log(`[LOG] ${new Date().toISOString()} ${message}`);
    }
  };
}

// 基类
class Product {
  constructor(public name: string, public price: number) {}
}

// 叠加三个 Mixin
const EnhancedProduct = Loggable(Serializable(Timestamped(Product)));

const p = new EnhancedProduct("机械键盘", 399);
p.log("商品已创建");
console.log(p.serialize());
// {"name":"机械键盘","price":399,"createdAt":"2026-08-10T..."}

嵌套的顺序是从内到外执行:先 Timestamped,再 Serializable,最后 Loggable。每个 Mixin 都在前一个的基础上扩展。

Tip

嵌套写法 Loggable(Serializable(Timestamped(Product))) 读起来不太舒服。你可以写一个组合函数来简化,但这不是 TypeScript 语言层面的特性,而是你自己的工具函数。这里保持简单,不展开。

带约束的 Mixin

有时候你的 Mixin 需要依赖基类提供某些属性或方法。比如”可跳”的 Mixin 需要基类有 setPos() 方法:

type Constructor<T = {}> = new (...args: any[]) => T;

// 约束类型:基类必须有 setPos 方法
type Positionable = Constructor<{ setPos(x: number, y: number): void }>;

function Jumpable<TBase extends Positionable>(Base: TBase) {
  return class extends Base {
    jump(): void {
      // 因为约束了 Base 必须有 setPos,所以 this.setPos 是安全的
      this.setPos(0, 20);
      console.log("跳起来了!");
    }
  };
}

// 基类满足约束
class Sprite {
  private x = 0;
  private y = 0;

  setPos(x: number, y: number): void {
    this.x = x;
    this.y = y;
  }

  getPos(): { x: number; y: number } {
    return { x: this.x, y: this.y };
  }
}

const JumpingSprite = Jumpable(Sprite);
const sprite = new JumpingSprite();
sprite.jump();
console.log(sprite.getPos()); // { x: 0, y: 20 }

如果基类不满足约束,TypeScript 在编译时就会报错:

class PlainObject {
  // 没有 setPos 方法
}

// const Bad = Jumpable(PlainObject);  // ❌ 类型不满足 Positionable 约束

Mixin 与接口结合

Mixin 返回的类可以在类表达式中声明 implements,让消费方通过接口类型来操作:

type Constructor<T = {}> = new (...args: any[]) => T;

interface Serializable {
  serialize(): string;
}

interface Cloneable<T> {
  clone(): T;
}

function SerializableMixin<TBase extends Constructor>(Base: TBase) {
  return class extends Base implements Serializable {
    serialize(): string {
      return JSON.stringify(this);
    }
  };
}

function CloneableMixin<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    clone(): this {
      return Object.assign(
        Object.create(Object.getPrototypeOf(this)),
        this
      );
    }
  };
}

class Document {
  constructor(public title: string, public content: string) {}
}

const RichDocument = CloneableMixin(SerializableMixin(Document));

const doc1 = new RichDocument("TypeScript 入门", "正文内容...");
const doc2 = doc1.clone();
doc2.title = "TypeScript 进阶";

console.log(doc1.serialize());
console.log(doc2.serialize());
console.log(doc1.title !== doc2.title); // true —— 独立副本
Note

Mixin 返回的类表达式中,不能用 privateprotected 修饰符(因为类表达式在 Mixin 函数的闭包里,TypeScript 无法跨闭包跟踪这些修饰符)。如果需要私有状态,用 JavaScript 的 # 私有字段替代。

Mixin vs 其他方案

选择 Mixin 之前,想想是否有更简单的方案。

Mixin vs 多继承

TypeScript 不支持多继承,这是 JavaScript 语言的限制,不是 TypeScript 的。Mixin 是单继承限制下的变通方案。

Mixin vs 组合(Composition)

组合是”一个类包含另一个类的实例”,而不是继承它:

// 组合方式:不用 Mixin,直接持有实例
class Logger {
  log(message: string): void {
    console.log(`[LOG] ${message}`);
  }
}

class UserService {
  private logger = new Logger();  // 组合

  createUser(name: string): void {
    // 业务逻辑...
    this.logger.log(`用户 ${name} 已创建`);
  }
}
维度Mixin组合
使用方式this.log() 直接调用this.logger.log() 通过属性调用
类型层面混合后的类有 Mixin 的方法签名依赖 Logger 类型,更独立
灵活性类定义时决定,改不了可以运行时替换 Logger 实例
复杂度泛型约束带来额外复杂度简单直接

组合通常更简单、更灵活。只有在 Mixin 提供的方法是你希望”就像这个类自己的方法”(比如 obj.clone() 而不是 obj.cloner.clone())时,才考虑 Mixin。

Mixin vs 装饰器

装饰器也能给类添加行为,但它更偏向”元编程”——在类定义时拦截和修改。Mixin 更偏向”组合”——把独立的能力拼在一起。

Note

TypeScript 的 Legacy 装饰器(需 experimentalDecorators)仍处于实验阶段;而 TC39 Stage 3 标准装饰器自 TypeScript 5.0 起已稳定可用,无需额外配置。两种装饰器语法不同,详见第 67-71 章。装饰器的话题在后面章节会详细展开,这里不深入。

实用建议

Mixin 是一个”高级功能”,不是每个项目都需要。给你一个判断清单:

用 Mixin 的场景:

  • 多个不相关的类需要共享同一组行为,且这些行为有实质代码需要复用。
  • 你希望这些行为像原生方法一样直接调用(obj.serialize() 而不是 obj.serializer.serialize())。
  • 行为之间有依赖关系,且可以通过泛型约束表达清楚。

不用 Mixin 的场景:

  • 行为只需要在一个继承链上共享——用普通继承或抽象类。
  • 只需要定义签名、不需要共享代码——用接口 + implements
  • 行为之间完全独立,可以独立实例化——用组合。
  • 只是想把不同类的代码”拼在一起”——停下来想想是否可以先抽成普通函数。

小结

  • Mixin 是单继承限制下的多行为组合方案,核心是”接收基类、返回扩展类”的泛型函数。
  • Constructor<T = {}> 是 Mixin 模式的基础类型,描述”任意可实例化的类”。
  • 多个 Mixin 可以嵌套叠加,顺序从内到外。
  • 通过泛型约束可以限制 Mixin 只能混入满足特定条件的基类。
  • Mixin 返回的类可以实现接口,让消费方通过接口类型操作。
  • 类表达式中不能用 private/protected,需要时用 JS 的 # 私有字段。
  • 能用组合解决的问题,优先用组合。Mixin 是组合不够用(需要”像自己的方法一样调用”)时的备选。

到这里,TypeScript 类的全部内容就讲完了。从基本语法到访问修饰符,从继承多态到抽象类,从静态成员到接口实现,再到更灵活的 Mixin 模式——你已经掌握了用 TypeScript 写面向对象代码的完整工具箱。下一章,我们将进入 TypeScript 另一个强大的领域:泛型。