JavaScript 继承
原型链继承
上篇 《JavaScript 中的构造函数及原型模式》 的文章里提到了只要创建了一个新函数,就会为该函数创建一个 prototype 属性(指向函数的原型对象),所有的原型对象都会自动获得一个 constructor 属性,这个属性包含一个指向构造函数的指针。下面先看代码:
1 | function One() { |
上面的代码将继承函数 Two() 的 prototype 直接修改为了构造函数 One() 的实例,继承了 One() 的方法,所以实例 two 在调用 sayName() 方法会直接输出 xiaoming,而被继承函数 One() 的 num 属性也被继承了下来。但是当第二个实例 two2 修改 num 属性的时候,第一个实例 two 的 num 属性也被随之修改了,因为所有实例共享的是一个 num 属性,这就是原型链继承的缺点。当然这个缺点只是针对引用数据类型,而基本数据类型不受这个影响,这一点可以从 a 属性的修改中看出。
借用构造函数
直接看代码吧:
1 | function One(name) { |
上面的继承函数 Two() 使用了 call() 方法,使用了被继承函数 One() 的方法替换了自己的方法,所以实例 two 在调用 sayName() 的时候成功输出”xiaoming”,而且在调用方法的时候还可以传参数进去,这里也可以使用 apply() 方法,与 call() 不同的是 apply() 的第二个参数是一个数组,call() 可以只传入第一个参数,也可以在第一个参数后依次传入多个参数。
使用这种方法也实现了继承,但是这种方法也有缺点,就是继承函数只能继承构造函数内的属性和方法,原型内的属性和方法对于继承函数来说是不可见的。这就是为什么最后调用 two.sayHi() 方法会报错了。
组合继承
《JavaScript 高级程序设计》一书中说组合继承有时也被叫做伪经典继承,指的是将原型链继承和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。这样既通过在原型定义方法实现了函数的复用,又能保证每个实例都有它自己的属性。
1 | function One(name) { |
这种继承方法就是将前两种继承方法组合到了一起,集合了两种方法的优点,既能保证多个被继承实例拥有各自的属性或方法(如上面代码中的 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 | function One(name) { |
我们用了一个 Object.create() 方法,这个方法会创建一个新对象,并使用第一个参数提供的现有对象来作为新创建对象的 __proto__,并将这个新对象作为返回值。具体实现如下(本方法有两个参数,下面展示的只是第一个参数的实现,摘自《JavaScript 高级程序设计》6.3.4):
1 | function object(o) { |
我们将 One.prototype 作为 Object.create() 的第一个参数执行后赋值给了 Two.prototype,这时 num 数组自然可以得到继承。下面一行我们重新把 Two 的构造函数指回了它自己,使得后面传入的 name 属性也可以被创建。
所以在 ES5 中,寄生组合式继承算是一个相对完美的继承方法。
class 继承
ES6 中新引入了 class 关键字,并且加入了 class 继承。这种方法通过 extends 来实现继承,减少了使用继承的代码量,使代码变得更加清晰易懂。
1 | class Person { |
上面的代码先用 class 关键字定义了 Person,然后 Xm 用 extends 继承了 Person。这里需要注意的是,子类在继承父类的时候必须先在自己的构造函数(constructor)里调用 super 方法,而且要写在 this 之前,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。
如果子类没有定义 constructor 方法,那这个方法会被默认添加。也就是说,不管有没有显式定义,任何子类都有 constructor 方法。