JS中的组合与继承
在前端开发中,有什么是继承可以做到而组合做不到的呢?
经典的EventTarget可以同时使用两种方式实现
typescript
// class
export class EventEmitter {
protected eventMap = new Map<string, Function[]>()
on(name: string, callback: (...args: any[]) => void) {
this.eventMap.set(name, [...this.eventMap.get(name) ?? [], callback])
}
off(name: string, callback: any) {
this.eventMap.set(name, (this.eventMap.get(name) ?? []).filter(c => c !== callback))
}
emit(name: string, ...args: any[]) {
this.eventMap.get(name)?.forEach((callback) => {
callback(...args)
})
}
}
// functional
export const createEventEmitter = () => {
const eventMap = new Map<string, Function[]>();
const on = (name: string, callback: (...args: any[]) => void) => {
eventMap.set(name, [...eventMap.get(name) ?? [], callback])
}
const off = (name: string, callback: any) => {
eventMap.set(name, (eventMap.get(name) ?? []).filter(c => c !== callback))
}
const emit = (name: string, ...args: any[]) => {
eventMap.get(name)?.forEach((callback) => {
callback(...args)
})
}
return {
on, off, emit
}
}
看起来似乎都可以实现功能,只是换了种写法。
现在考虑一个父类,除了eventEmitter功能之外,还需要一些别的功能:
typescript
// class
export class Parent extends EventEmitter {
someMethod() { }
}
// functional
export const createParent = () => {
const evenEmitter = createEventEmitter()
const someMethod = () => { }
return {
...evenEmitter,
someMethod
}
}
似乎依旧并无大碍,组合只是代码稍微多了一丢丢。
那么继续,现在父类需要暴露自己所有已注册的监听器名称
typescript
export class Parent extends EventEmitter {
someMethod() { }
get eventNames(){
return [...this.eventMap.keys()]
}
}
于是,组合不适应的地方出现了。因为组合只有私有变量和公开变量(贴合class的说法),因此在多重嵌套下,要想用到内部某个函数的私有变量,只能从最底部开始,让其重新暴露出一个新的变量
typescript
export const createEventEmitter = () => {
// ...
return {
// 将eventMap作为公开变量暴露出来
on, off, emit, eventMap
}
}
export const createParent = () => {
const { on, off, emit, eventMap } = createEventEmitter()
const someMethod = () => { }
return {
on, off, emit,
someMethod,
get eventNames() {
return [...eventMap.keys()]
}
}
}
但是,这样的弊端就出现了,eventMap作为关键变量被危险地暴露了出来,现在这个组合不再安全,eventEMitter的eventMap可能会在其他地方被随意修改,监听器变得不再可靠。
这只是一些特意举出来的例子,事实上如何使用组合与继承是一门哲学,并非完全的谁一定好于谁。在大多数情况下,某个对象并不会一层层地基于另一个对象,使用组合不仅能良好地组织代码,帮助梳理各个对象间的关系,还可以提高代码阅读效率,毕竟,组合天然就具有多继承的特性:
typescript
export const createAnimal = () => {
return {
eat: () => {
console.log('eat')
}
}
}
// 可以轻松实现Animal与EventMitter的“杂交”
export const createParent = () => {
const animal = createAnimal()
const eventEmitter = createEventEmitter()
const someMethod = () => { }
return {
...eventEmitter,
...animal,
someMethod
}
}
// Error, Javascript不支持多继承
export class Parent extends EventEmitter, Animal {
someMethod() { }
get eventNames() {
return [...this.eventMap.keys()]
}
}
换句话说,组合更适合于平铺,适合于把多个解决方法合并起来;而继承适合嵌套,更多用在概念上的层层递进。当然,这里仅限于Javascript,因为Javascript天然没有多继承。