为什么我讨厌 JS 中的继承?
May 26, 2023
使用继承
需求
现在要给 Person 添加功能
1 | person1.on('die', fn) |
简单,加代码
1 | class Person { |
let person1 = new Person()
,这样 person1
就既是人类,又能发布订阅
目前来说没有问题,因为代码没有重复
另一个类:报社
现在除了人类之外,报社也要拥有发布订阅功能
1 | class 报社{ |
let 报社1 = new 报社('新华日报')
,这样,报社1
就既是报社,又能发布订阅
那么问题来了,代码有重复了!
消除重复
Person 和报社有重复属性
- 把重复属性求出来,单独写个类
EventEmitter
- 然后让
Person
和报社
继承EventEmitter
细节
constructor
要调用super()
- 以保证
EventEmitter
实例被初始化
代码
1 | class EventEmitter{ |
继承的其他功能
重写
- 子类重写父类的所有属性,以实现多态
- 多态的意思是不同的子类对同一个消息有不同的反应
1 | class Person extends EventEmitter{ |
但是,继承有个问题。
继承有什么问题
如果我需要更多功能怎么办?
两个选择:
让
EventEmitter
继承其他类
比如让
发布订阅
去继承跑
,那是不是很奇怪?让
Person
继承两个类
(多继承)很遗憾这个功能在
JavaScript
和Java
中都没有,C++
中有
所以这两个方法都走不通
你可能会说: “那可以把这两个再抽象一下呀”,但是下面讲的的组合,你就会发现 继承
就是一个笑话!
使用组合
类
是固定写法,把语法已经写死了,而组合
没有固定写法,在这里我写我认为的组合写法,可能和你想的不一样,我这里只演示一种
让 Person 实现发布订阅
1 | class Person { |
以上代码就是不使用继承,但拥有发布订阅的功能
这样功能是不是实现了?只不过目前的代码有点挫,但是可以优化呀
优化代码
1 | class Person{ |
出现 Bug
你会发现,mixin
之后,EventEmitter
里的函数却拷贝不过去,这是因为 JS
的 class
有一个毛病:他会把函数放在原型上面,就无法枚举
那么我们可以这么做:
改写 EventEmitter
1 | function createEventEmitter() { |
那就拥有了 on()
off()
emit()
,当你把 继承
给干掉,你就会发现:这个世界扁平起来了
你需要什么,比如上面那 3 个 Api,我就把这 3 个 Api 的地址放到你身上,你想要其他功能,我就再把其他功能的地址放到你身上,这样就可以不断地去添加不同的功能
这就是
组合
的思想:你要什么东西,我就把什么东西的地址复制给你
如果你需要更多的功能
那就变得非常简单
1 | class Person { |
你看!我还需要什么继承?而且组和这 3 个功能合集之间没有关系,它们的关系是扁平的
所以由此得出:有了组合,你可能不需要 class,直接使用:函数 + 闭包
变态需求
猫、狗、动物
1 | dog |
那么猫和狗都可以拉屎,那我们就可以继承它们了
1 | // 伪代码,狗和猫除了都可以拉屎之外(继承了 poop()),还可以汪汪叫/喵喵叫 |
扫地机器人
那么动物会拉屎,那么这个时候需要一个机器人来打扫卫生
1 | cleaningRobot |
两个机器人
我想让机器人可以s人
1 | murderRobot |
那么上面代码会发现,s人机器人和扫地机器人它们都可以跑,那么就可以抽象成以下代码:
1 | // 伪代码,s人机器人和扫地机器人都继承机器人可以跑的功能 |
终极变态需求
那么我想要一个:狗形s人机器人
那么看看我们如何解决这个需求
1 | # 伪代码 |
那么请问:这个需求我们如何用继承来解决?是不是很难?因为狗是继承了 animal 的拉屎呀!所以想用继承实现,是基本做不到的,或者说是非常难!
继承做不到,那么我们使用组合
那就太简单了,让我们把功能组合一下
dog = poop() + wangwang()
cat = poop() + miaomiao()
cleaningRobot = run() + clean()
狗形s人机器人 = run() + kill() + wangwang()
不用 class
代码怎么写?
不用 class
写 Dog
1 | const createWang = state => ({ |
那么现在运行一下以上代码:
那我们来实现狗形s人机器人:
1 | const createMurderRobot = name => { |
你看!这种办法是不是让我们的代码组合非常的灵活?
这就是组合模式
运用场景
难道我们就不用继承了吗?
当然不是
什么时候用继承
场景
- 开发者不会用组合
- 功能没有太多交叉的地方,一眼就能看出继承关系
- 前端库比较喜欢用继承
举例
const vm = new Vue({...})
- Vue 继承了 EventEmitter
class App extends React.Component{...}
- React.Component 内置了方便的复用功能(钩子)
什么时候用组合
场景
- 功能组合非常灵活,无法一眼看出继承关系
- 插件系统
- mixin 模式
举例
Vue.mixin()
Vue.use(MyPlugin)
- React 接受组件的组件
缺点
写法太灵活,我这里写的组合可能在其他人那里就不认同
总结
但不管怎么写,反正最终前端程序员基本上达成一个共识:组合是优于继承的
原因是组合能够实现扁平化的分配,而继承是垂直而,当出现交叉的时候,继承就基本做不到
一些吐槽
前端程序员,在很早之前是被 Java
程序员所占领的(没有人学前端,只有学 Java 的人最接近前端),Javaers 就把面向对象、class 带到前端
那个时候作为一名前端新人进到一个团队,那就看到面前有几位 Java 工程师转的前端,他们认为写代码应该用继承、用多态封装,然后你就抄他们的代码,然后抄着抄着发现面前的代码越写越复杂
因为前端它的特点就是 —— 需求很灵活,不像后端有些数据结构是非常固定的,前端你可能随时都能新增一些数据
这样的话前端就开始想:那会不会不是我的问题,而是面向对象的问题,为什么面向对象的继承这么难用,一旦出现交叉的数据,就搞不出来
所以就开始往其他的方向探索,其中非常大的方向就是函数式编程,函数式编程一般来说就不需要「经典的 class」
比如以上的代码,我就用函数来创造对象,我为什么要用 new?new 是一个前端程序员非常难以理解的东西
感谢阅读,下次见 :)
cd ../