ES5和ES6继承对比

2020/05/25 Javascript 共 2245 字,约 7 分钟

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))
  • 继承在前,实例在后。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关键字
    • 这里所谓的多态调用底层集中是指,在显式多态调用中,我们通过callapply等方式,显式调用祖先类并绑定当前子类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,如果superthis类似为动态绑定的,那么执行E.foo()应该打印D.foo 但是因为super是静态绑定的,所以 super总是绑定到链中的上一层,在这里是P,所以总会打印P.foo

但我个人觉得这个是完全可以接受的,诸如动态this这种实现,本身就是利弊都有的;super于class的实现我们完全可以类比为lambda函数中的this的实现,不能因为class本质是基于prototype委托的语法糖实现,就要求class具有prototype的动态绑定特性


[1] 实例化与原型链变更

[2] 原型属性屏蔽

[3] super关键字

[4] extends关键字

Search

    Table of Contents