Skip to content
大纲

实现继承 ES5/ES6

可以参看《JavaScript 高级程序设计》第四版 P238

  1. 组合继承
  2. 原型式继承
  3. 寄生式继承
  4. 寄生式组合继承
  5. 最终方案 (实现一个你认为不错的 js 继承方案)
  6. ES6 继承

创建对象

  • Object 构造对象,即 new Object()
  • 对象字面量,即 var o = { // 定义属性或方法 }

工厂模式

js
function create(name) {
  var o = new Object()
  o.name = name
  o.sayHi = function () {
    console.log(thi.name)
  }
  return o
}
var person1 = create('kim')

缺点:没有解决对象识别的问题(即怎样知道一个对象的类型,instanceof)

构造函数模式

js
function Person(name, age) {
  this.name = name
  this.age = age
  this.sayHi = function () {
    console.log(`Hi, My name is ${this.name}`)
  }
}

let person1 = new Person('kim', 22)
let person2 = new Person('Lee', 21)
console.log(person1 instanceof Object) // true
console.log(person1 instanceof Person) // true
console.log(person2 instanceof Object) // true
console.log(person2 instanceof Person) // true

缺点:每个方法都要在每个实例上重新创建一遍。this.sayHi = function () {} 等价于 this.sayHi = new Function();注意:构造函数以大写字母开头,而且必须使用 new 操作符实例对象。调用构造函数会经历一下 4 个步骤:

  • 创建一个新对象;
  • 将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象。

构造函数胜于工厂函数的地方是:创建自定义构建函数都有一个 constructor(构造函数)属性,用来标识对象类型,而且将它的实例标识为一种特定的类型,即可以通过 instanceof 来检查对象类型。

ES5 继承

组合继承

组合继承(有时候也叫做伪经典继承)综合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数集成实现属性。这样既可以把方法定义在原型上实现重用,又可以让每个实例都有自己的属性。

js
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.sayName = function () {
  console.log(this.name)
}

function SubType(name, age) {
  // 继承属性
  SuperType.call(this, name)

  this.age = age
}

// 继承方法
SubType.prototype = new SuperType()

SubType.prototype.sayAge = function () {
  console.log(this.age)
}

let instance1 = new SubType('xiaoming', 18)
instance1.colors.push('yellow')
console.log(instance1.colors)

instance1.sayName()
instance1.sayAge()

let instance2 = new SubType('xiaohong', 19)
console.log(instance2.colors)
instance2.sayName()
instance2.sayAge()

组合继承弥补了原型链和盗用构造函数的不足,是 JavaScript 中使用最多的的继承模式。而且组合继承也保留了 instanceof 操作符和 isPrototypeof() 方法识别合成对象的能力。

原型式继承

2006 年,Douglas Crockford 写了一篇文章:《JavaScript 中的原型式继承》(“Prototypal Inheritance in JavaScript”)。这篇文章介绍了一种不涉及严格意义上构造函数的继承方法。他的出发点是即使不自定义类型也可以通过原型实现对象之间的信息共享。文章最终给出了一个函数:

js
function object(o) {
  function F() {}
  F.prototype = o
  return new F()
}

这个 object() 函数会创建一个临时构造函数,将传入的对象赋值给这个构造函数的原型,然后返回这个临时类型的一个实例。本质上,object() 是对传入的对象执行了一次浅复制。

Crockford 推荐的原型式继承适用于这种情况:你有一个对象,想在它的基础上再创建一个新对象。

寄生式继承

与原型式继承比较接近的一种继承方式是寄生式继承(parasitic inheritance),也是 Crockford 首倡的 一种模式。寄生式继承背后的思路类似于寄生构造函数和工厂模式:创建一个实现继承的函数,以某种 方式增强对象,然后返回这个对象。基本的寄生继承模式如下:

js
function createAnother(original) {
  let clone = object(original) // 通过调用函数创建一个新对象
  clone.sayHi = function () {
    // 以某种方式增强这个对象
    console.log('hi')
  }
  return clone // 返回这个对象
}

寄生式组合继承

目前普遍认为是引用类型最理想的继承范式。

组合继承其实也存在效率问题。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用。本质上,子类原型最终是要包含超类对象的所有实例属性,子类构造函数只要在执行时重写自己的原型就行了。再来看一看这个组合继承的例子:

js
function SuperType(name) {
  this.name = name
  this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function () {
  console.log(this.name)
}
function SubType(name, age) {
  SuperType.call(this, name) // 第二次调用 SuperType()
  this.age = age
}
SubType.prototype = new SuperType() // 第一次调用 SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function () {
  console.log(this.age)
}

代码中高亮部分是调用 SuperType 构造函数的地方。

问题

ES5、ES6 的继承除了写法以外,还有什么区别?

子类 this 生成顺序不同

  • ES5 先生成子类实例,再调用父类的构造函数修饰子类实例
  • ES6 先生成父类实例,再调用子类的构造函数修饰父类实例
    • 这个差别使得 ES6 可以继承内置对象

为什么子类的构造函数,一定要调用 super()?

原因就在于 ES6 的继承机制,与 ES5 完全不同。

  • ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
  • ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。

这就是为什么 ES6 的继承必须先调用 super() 方法,因为这一步会生成一个继承父类的 this 对象,没有这一步就无法继承父类。

注意,这意味着新建子类实例时,父类的构造函数必定会先运行一次。