先记住这个答案
JavaScript 的 class 只能 extends 一个父类。如果鸭子既要「能飞」又要「能游泳」,这两组方法没法都放在父类里,就用 mixin:把一组方法单独定义,再「混」进需要它的类。推荐的写法是类工厂,也就是一个接收父类、返回子类的函数,比如 const CanFly = (Base) => class extends Base { fly() {} },然后写 class Duck extends CanSwim(CanFly(Animal))。每个 mixin 在原型链上占一层,super 和 instanceof 都正常工作。
- mixin 可向类叠加多组能力
- 类工厂可逐层扩展基类
Object.assign不复制不可枚举类方法- 同名方法后混入的覆盖先混入的
类工厂写法:一层层包起来
类工厂的思路是「继承链还是一条,只是中间多插了几层」。
const CanFly = (Base) => class extends Base {
fly() { return `${this.name} 在飞` }
}
const CanSwim = (Base) => class extends Base {
swim() { return `${this.name} 在游泳` }
}
class Animal {
constructor(name) { this.name = name }
}
// 从里往外包:Animal → CanFly → CanSwim → Duck
class Duck extends CanSwim(CanFly(Animal)) {}
const duck = new Duck('唐老鸭')
console.log(duck.fly())
console.log(duck.swim())
console.log(duck instanceof Animal) // 链条还是完整的查看输出与解释
唐老鸭 在飞
唐老鸭 在游泳
trueCanFly(Animal) 返回一个继承 Animal 的匿名类,CanSwim 再继承它,Duck 继承最外层。所以 duck 的原型链是 Duck.prototype → CanSwim 层 → CanFly 层 → Animal.prototype。方法都在各自那一层的原型上,所有实例共享,不会每个实例复制一份。
因为是真的继承,mixin 里可以写 super.method() 调用下一层的同名方法,多个 mixin 都重写 init 时就能串起来执行,这是另一种写法做不到的。
Object.assign 写法和它的坑
更早的写法是把方法写在普通对象里,然后 Object.assign(Duck.prototype, flyMixin, swimMixin)。它简单直接,Vue 2 的 mixins 选项、很多老库都是这个思路。
但它有几个限制。第一,方法是复制过去的,mixin 里没法用 super 调用父类方法,对象字面量方法里的 super 在复制之后仍然指向原来的对象。第二,只复制自身可枚举属性,getter 会被执行,写过去的是当时的值,getter 本身丢了。第三,同名方法直接覆盖,后面的 mixin 悄悄盖掉前面的,没有任何提示。
- 为什么 assign 一个 class 的原型什么都拿不到
- 有人会把 mixin 也写成 class,然后
Object.assign(Duck.prototype, CanFly.prototype),结果duck.fly是undefined。原因是 class 里定义的方法都是不可枚举的,而Object.assign只复制可枚举属性。要么把 mixin 写成普通对象,要么用Object.getOwnPropertyNames遍历,再用Object.defineProperty逐个复制描述符。
容易答错的地方
- 以为 mixin 就是多继承
- JavaScript 里没有真正的多继承。类工厂只是把多个 mixin 排成一条线性链,顺序决定了同名方法谁覆盖谁:写在外层的 mixin 离子类更近,优先被找到。改变嵌套顺序,行为可能就变了。
- mixin 用得越多越好
- mixin 多了,一个方法到底来自哪一层很难追,名字也容易冲突。React 早期的
createClass支持 mixins,后来官方明确不推荐,改用高阶组件,最后是 Hooks。能用组合解决的,比如把一个logger对象作为属性传进来,就不要用 mixin。
面试官还会怎么问?
两个 mixin 都定义了 init 怎么办?
类工厂写法里,让每个 mixin 的 init 先调用 super.init?.() 再做自己的事,调用就会沿着链一层层传下去,全部执行。Object.assign 写法没有这个能力,只能在混入前手动检测冲突,或者约定方法名加前缀。
TypeScript 里 mixin 怎么写类型?
官方文档给的写法是先定义构造函数类型 type Constructor = new (...args: any[]) => {},mixin 函数写成 function CanFly<TBase extends Constructor>(Base: TBase),返回 class extends Base。这样组合出来的类,TypeScript 能推断出所有混入的方法。
Vue 3 为什么不再推荐 mixins?
mixins 选项的问题是来源不清和命名冲突:模板里用到一个 loading,看不出它来自哪个 mixin,两个 mixin 定义同名数据还会合并或覆盖。Vue 3 推荐用组合式函数,const { loading } = useFetch() 返回值显式解构,来源一目了然,重名时自己改个名字就行。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。