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天然没有多继承。