JavaScript 继承

原型链继承

  上篇 《JavaScript 中的构造函数及原型模式》 的文章里提到了只要创建了一个新函数,就会为该函数创建一个 prototype 属性(指向函数的原型对象),所有的原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向构造函数的指针。下面先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function One() {
this.a = 'a'
this.num = [1, 2]
}
One.prototype.sayName = function () {
console.log('xiaoming')
}
function Two() {
}
Two.prototype = new One() // 直接把继承函数prototype指向的构造函数改为被继承的构造函数

var two = new Two()
two.sayName() // 控制台打印"xiaoming",继承了One()的方法
var two2 = new Two()
two2.num.push(3)
two2.a = 'b'

two2.num // 输出[1, 2, 3]
two.num // 输出[1, 2, 3]
two2.a // 输出b
two.a // 输出a

  上面的代码将继承函数 Two() 的 prototype 直接修改为了构造函数 One() 的实例,继承了 One() 的方法,所以实例 two 在调用 sayName() 方法会直接输出 xiaoming,而被继承函数 One() 的 num 属性也被继承了下来。但是当第二个实例 two2 修改 num 属性的时候,第一个实例 two 的 num 属性也被随之修改了,因为所有实例共享的是一个 num 属性,这就是原型链继承的缺点。当然这个缺点只是针对引用数据类型,而基本数据类型不受这个影响,这一点可以从 a 属性的修改中看出。

借用构造函数

  直接看代码吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function One(name) {
this.name = name
this.sayName = function() {
console.log(this.name)
}
}
One.prototype.sayHi = function() {
console.log('Hi!')
}
function Two() {
One.call(this, 'xiaoming')
}

var two = new Two('xiaoming')
two.sayName() // 控制台打印"xiaoming"
two.sayHi() // 报错

  上面的继承函数 Two() 使用了 call() 方法,使用了被继承函数 One() 的方法替换了自己的方法,所以实例 two 在调用 sayName() 的时候成功输出”xiaoming”,而且在调用方法的时候还可以传参数进去,这里也可以使用 apply() 方法,与 call() 不同的是 apply() 的第二个参数是一个数组,call() 可以只传入第一个参数,也可以在第一个参数后依次传入多个参数。

  使用这种方法也实现了继承,但是这种方法也有缺点,就是继承函数只能继承构造函数内的属性和方法,原型内的属性和方法对于继承函数来说是不可见的。这就是为什么最后调用 two.sayHi() 方法会报错了。

组合继承

  《JavaScript 高级程序设计》一书中说组合继承有时也被叫做伪经典继承,指的是将原型链继承和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。这样既通过在原型定义方法实现了函数的复用,又能保证每个实例都有它自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function One(name) {
this.name = name
this.num = [1, 2]
}
One.prototype.sayHi = function() {
console.log('Hi!')
}
function Two(name) {
// 继承属性
One.call(this, name)
}
Two.prototype = new One() // 继承方法

var two = new Two('xkf')
var two2 = new Two('xkf')
two.name // 输出'xkf'
two2.num.push(3)
two.num // 输出[1, 2]
two2.num // 输出[1, 2, 3]
two.sayHi === two2.sayHi // true

  这种继承方法就是将前两种继承方法组合到了一起,集合了两种方法的优点,既能保证多个被继承实例拥有各自的属性或方法(如上面代码中的 num 属性),又能共享原型中的属性或方法(上面的 sayHi() 方法返回 true 说明两个实例共享一个方法),是比较常用的一种继承方法。

寄生组合式继承

  在 ES5 中,尽管组合继承看起来很完美,但是也有缺点。上面组合继承方法内示例代码的第 10 行 One.call(this, name) 和第 12 行 Two.prototype = new One(),我们分开来说。第 12 行先是将 Two.prototype 指向了 One 的实例,这时 Two 的 prototype 得到了 num 数组(name 属性的值需要传入,不考虑第 10 行的话这个属性并没有获取到),这是第一次调用构造函数 One。第 10 行采用了构造函数继承的方法,这个时候 num 数组又被创建了一次,这第二次调用构造函数 One 创建的 num 数组屏蔽了上一次原型内的 num 数组,所以这两次调用导致了数组 num 被创建了两次,造成了不必要的浪费。

  所以我们可以使用寄生组合式继承的方法来实现较完美的继承。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function One(name) {
this.name = name
this.num = [1, 2]
}
One.prototype.sayHi = function () {
console.log('Hi!')
}
function Two(name) {
// 继承属性
One.call(this, name)
}
Two.prototype = Object.create(One.prototype) // 调用Object.create()方法来修改原型
Two.prototype.constructor = Two // 将构造函数指回自己

var two = new Two('xkf')
var two2 = new Two('xkf')
two.name // 输出'xkf'
two2.num.push(3)
two.num // 输出[1, 2]
two2.num // 输出[1, 2, 3]
two.sayHi === two2.sayHi // true

  我们用了一个 Object.create() 方法,这个方法会创建一个新对象,并使用第一个参数提供的现有对象来作为新创建对象的 __proto__,并将这个新对象作为返回值。具体实现如下(本方法有两个参数,下面展示的只是第一个参数的实现,摘自《JavaScript 高级程序设计》6.3.4):

1
2
3
4
5
function object(o) {
function F() { }
F.prototype = o
return new F()
}

  我们将 One.prototype 作为 Object.create() 的第一个参数执行后赋值给了 Two.prototype,这时 num 数组自然可以得到继承。下面一行我们重新把 Two 的构造函数指回了它自己,使得后面传入的 name 属性也可以被创建。

  所以在 ES5 中,寄生组合式继承算是一个相对完美的继承方法。

class 继承

  ES6 中新引入了 class 关键字,并且加入了 class 继承。这种方法通过 extends 来实现继承,减少了使用继承的代码量,使代码变得更加清晰易懂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
sayName() {
console.log(this.name)
}
}
class Xm extends Person {
constructor(name, age, job) {
super(name, age) // 使用"super"来调用父类Person中的name和age,且必须在this之前
this.job = job
}
sayHi() {
console.log('Hi!')
}
}

var xm = new Xm('xiaoming', 24, 'Engineer')

  上面的代码先用 class 关键字定义了 Person,然后 Xm 用 extends 继承了 Person。这里需要注意的是,子类在继承父类的时候必须先在自己的构造函数(constructor)里调用 super 方法,而且要写在 this 之前,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。

  如果子类没有定义 constructor 方法,那这个方法会被默认添加。也就是说,不管有没有显式定义,任何子类都有 constructor 方法。