ES5和ES6继承对比
1. new|Object.create|Object.setPrototypeOf
三者总结:
new构造调用
- 缺点比较明显:
- 容易发生属性屏蔽问题,参考原型属性屏蔽
- 容易引发
constructor
副作用问题,因为会执行一个函数,所以可能发生副作用问题 - 有不同的实现方式(new、手动绑定、call、apply等),但都基于
prototype
和__proto__
,会引入相对混乱的原型关系
- 优点:
- 是ES5实现,支持广泛,使用简单
- 缺点比较明显:
Object.create
:- 优点比较明显:
constructor
无关,规避了可能发生的副作用问题- 创建了新的对象进行
prototype
委托关联,不修改原型,immutable的思想比较好接受
- 缺点:
- 因为使用immutable的方式进行原型变更,会导致原有原型的回收GC性能浪费
- 优点比较明显:
Object.setPrototypeOf
- 优点:
- 可以直接修改原型,而不是替换原型
- 缺点:
- 心智模型稍复杂,
Object.create
更容易接受
- 心智模型稍复杂,
- 优点:
- 三种实现都存在:
- 多态调用不够清晰,无法顶层管理,参考super关键字
ES5和ES6继承的核心区别:
- 实例在前,继承在后。ES5的继承,实质是先创造子类的实例对象this,然后将父类的方法添加到this上面(
Parent.apply(this)
)- 以
new
操作符构造继承为例,参考手写实现过程:实例化与原型链变更
- 以
- 继承在前,实例在后。ES6的继承,实质是先将父类实例对象的属性和方法添加到this上(所以必须先调用super方法),然后再用子类的构造函数修改this
1.1 【ES5】new构造调用继承:
优点:
- 实现简单,支持广泛
缺点:
- 依赖于
prototype
和__proto__
,相对繁琐杂乱 - 最大的副作用:会调用constructor
- 如
constructor
中可能包含有副作用的逻辑,如修改局部、全局变量,修改/添加this
属性等
- 如
1.2 【ES6】Object.create继承:
优点:
- 创建新的原型对象,并关联
prototype
- 不引入“new构造调用”可能发生的副作用问题,即继承过程
constructor
无关
缺点:
- 原型链替换会有轻微的GC性能浪费
1.3 【ES6】Object.setPrototypeOf继承
优点:
- 通过直接修改
prototype
的方式实现,GC相对友好
缺点:
- 心智模型稍复杂,同时
Object.create
的immutable的方式,更容易接受,也更简单
2. class extends继承
优点:
- 不引入杂乱的
prototype
和__proto__
,相对整洁。参考实例化与原型链变更 - 使用
super
关键字解决多态调用不能顶层集中的问题,同时super
的静态绑定特性,也更容易理解。参考super关键字- 这里所谓的多态调用底层集中是指,在显式多态调用中,我们通过
call
、apply
等方式,显式调用祖先类并绑定当前子类this
,调用地方变多以后,会变得难以管理,相对混乱;此时通过super静态绑定特性,始终指向父类,则我们可以使用super
关键字进行顶层集中管理,心智模型降低到一个关键字super
的维度。参考super关键字-解决原型链上层同名函数的多态调用问题
- 这里所谓的多态调用底层集中是指,在显式多态调用中,我们通过
- 可以简单的拓展原生类型。参考extends关键字
缺点:
- JS本身是没有类(class)的,现有的class也只是基于
prototype
实现的一种原型链委托语法糖 - 因为本质是一种委托机制,没有在定义时进行复制,假如我们意外修改了“父类”的一个方法,那么所有子类和实例都会受到影响
class
仍然会发生属性屏蔽问题,参考下方代码- 需要先调用
super
,再访问this
super
的静态绑定特性可能不够灵活,参考下方代码
// class属性屏蔽问题
class Foo {
constructor() {
// 此时发生了属性屏蔽问题,foo从function被屏蔽为了number
this.foo = 1
}
foo() {
console.log('this is foo fn!')
}
}
let foo = new Foo()
foo.foo() // main.js:10 Uncaught TypeError: foo.foo is not a function
// super的静态绑定问题
class P {
foo() {
console.log('P.foo')
}
}
class C extends P {
foo() {
super.foo()
}
}
let c1 = new C()
c1.foo() // P.foo
let D = {
foo: function () {
console.log('D.foo')
}
}
let E = {
foo: C.prototype.foo
}
// 将E.prototype设置为D
Object.setPrototypeOf(E, D)
E.foo() // P.foo
虽然我们已经设置E.prototype = D
,如果super
和this
类似为动态绑定的,那么执行E.foo()
应该打印D.foo
但是因为super
是静态绑定的,所以 super
总是绑定到链中的上一层,在这里是P,所以总会打印P.foo
但我个人觉得这个是完全可以接受的,诸如动态this这种实现,本身就是利弊都有的;super
于class的实现我们完全可以类比为lambda函数中的this
的实现,不能因为class本质是基于prototype
委托的语法糖实现,就要求class具有prototype
的动态绑定特性
[1] 实例化与原型链变更
[2] 原型属性屏蔽
[3] super关键字
[4] extends关键字