切换主题
Mixins 模式
在 JavaScript 中,我们只能继承单个对象。每个对象只能有一个 [[Prototype]]。并且每个类只可以扩展另外一个类。
但是有些时候这种设定(译注:单继承)会让人感到受限制。例如,我有一个 StreetSweeper 类和一个 Bicycle 类,现在想要一个它们的混合体:StreetSweepingBicycle 类。
或者,我们有一个 User 类和一个 EventEmitter 类来实现事件生成(event generation),并且我们想将 EventEmitter 的功能添加到 User 中,以便我们的用户可以触发事件(emit event)。
有一个概念可以帮助我们,叫做 "mixin"。
根据维基百科的定义,mixin 是一个类,其方法可被其他类使用,而无需继承。
换句话说,mixin 提供了实现特定行为的方法,但是我们不单独使用它,而是使用它来将这些行为添加到其他类中。
Mixin 基本实现
1. 对象 Mixin
javascript
// 一个 mixin
const sayMixin = {
say(phrase) {
console.log(phrase);
}
};
const sayHiMixin = {
sayHi() {
this.say(`Hello, ${this.name}`);
},
sayBye() {
this.say(`Bye, ${this.name}`);
}
};
// 复制方法
Object.assign(sayHiMixin, sayMixin);
// 然后将这些方法复制到用户类的原型中:
class User {
constructor(name) {
this.name = name;
}
}
// 复制方法
Object.assign(User.prototype, sayHiMixin);
// 现在 User 可以同时打招呼了
const user = new User("John");
user.sayHi(); // Hello, John!
user.sayBye(); // Bye, John!
2. 函数 Mixin
javascript
// Mixin 工厂函数
function mixin(targetClass, ...mixins) {
mixins.forEach(mixin => {
// 复制属性
Object.getOwnPropertyNames(mixin).forEach(property => {
if (property !== 'constructor' && property !== 'prototype') {
targetClass.prototype[property] = mixin[property];
}
});
});
return targetClass;
}
// 定义一些 mixin
const canSwim = {
swim() {
console.log(`${this.name} is swimming`);
}
};
const canFly = {
fly() {
console.log(`${this.name} is flying`);
}
};
// 基类
class Animal {
constructor(name) {
this.name = name;
}
}
// 创建一个既能游泳又能飞的鸭子类
const Duck = mixin(
class Duck extends Animal {},
canSwim,
canFly
);
const duck = new Duck("Donald");
duck.swim(); // Donald is swimming
duck.fly(); // Donald is flying
具有自己状态的 Mixin
Mixin 可以定义属性或状态,以及引用其他 mixin 的方法。
javascript
// Mixin
const eventMixin = {
// 向事件注册监听器
on(eventName, handler) {
if (!this._eventHandlers) this._eventHandlers = {};
if (!this._eventHandlers[eventName]) {
this._eventHandlers[eventName] = [];
}
this._eventHandlers[eventName].push(handler);
},
// 移除事件监听器
off(eventName, handler) {
const handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
for (let i = 0; i < handlers.length; i++) {
if (handlers[i] === handler) {
handlers.splice(i--, 1);
}
}
},
// 触发事件
trigger(eventName, ...args) {
const handlers = this._eventHandlers?.[eventName];
if (!handlers) return;
// 调用事件处理程序
handlers.forEach(handler => handler.apply(this, args));
}
};
// 使用 mixin
class Menu {
choose(value) {
this.trigger("select", value);
}
}
// 添加 mixin 的方法
Object.assign(Menu.prototype, eventMixin);
const menu = new Menu();
// 添加一个事件处理函数
menu.on("select", value => console.log(`选中值:${value}`));
// 触发事件
menu.choose("123"); // 选中值:123
使用 ES6 类实现 Mixin
1. 高阶组件模式
javascript
// 创建 mixin 函数
const Swimming = Base => class extends Base {
swim() {
console.log(`${this.name} is swimming`);
}
};
const Flying = Base => class extends Base {
fly() {
console.log(`${this.name} is flying`);
}
};
// 基类
class Animal {
constructor(name) {
this.name = name;
}
}
// 组合 mixin
const Bird = Flying(Animal);
const Fish = Swimming(Animal);
const Duck = Flying(Swimming(Animal));
const duck = new Duck("Donald");
duck.swim(); // Donald is swimming
duck.fly(); // Donald is flying
2. Symbol 防止方法冲突
javascript
// 使用 Symbol 作为方法名称防止冲突
const swimSymbol = Symbol('swim');
const flySymbol = Symbol('fly');
const swimmingMixin = {
[swimSymbol]() {
console.log(`${this.name} is swimming`);
}
};
const flyingMixin = {
[flySymbol]() {
console.log(`${this.name} is flying`);
}
};
class Animal {
constructor(name) {
this.name = name;
}
}
// 应用 mixin
Object.assign(Animal.prototype, swimmingMixin, flyingMixin);
const duck = new Animal("Donald");
duck[swimSymbol](); // Donald is swimming
duck[flySymbol](); // Donald is flying
使用场景与最佳实践
1. 何时使用 Mixin
- 需要在多个无关类之间共享功能
- 需要多重继承但 JavaScript 不直接支持
- 希望提高代码复用性
- 希望遵循组合优于继承的原则
2. 实现注意事项
- 避免方法名冲突
- 使用 Symbol 或命名约定避免冲突
- 谨慎覆盖已有方法
- 明确记录依赖关系
- 注意
this
的上下文
3. Mixin vs 继承
javascript
// 继承方式
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} is eating`);
}
}
class Mammal extends Animal {
breathe() {
console.log(`${this.name} is breathing`);
}
}
// vs Mixin 方式
class Animal {
constructor(name) {
this.name = name;
}
}
const eater = {
eat() {
console.log(`${this.name} is eating`);
}
};
const breather = {
breathe() {
console.log(`${this.name} is breathing`);
}
};
// 更灵活,可按需组合
Object.assign(Animal.prototype, eater, breather);
4. 最佳实践总结
- 明确职责分离:每个 mixin 应该专注于一个功能点
- 避免状态依赖:mixin 之间不应该有复杂的状态依赖关系
- 文档化:明确记录 mixin 的用途和依赖
- 命名约定:使用清晰的命名约定避免冲突
- 谨慎覆盖:避免盲目覆盖已有方法
- 使用 Symbol:考虑使用 Symbol 作为方法名
- 组合而非继承:优先考虑 mixin 组合而非深层继承
- 功能封装:mixin 应该是自包含的,不依赖于特定实现
- 测试隔离:应该能单独测试 mixin 的功能