原型链继承
上篇《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用intends
继承了Person
。这里需要注意的是,子类在继承父类的时候必须先在自己的构造函数(constructor)里调用super
方法,而且要写在this
之前,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
如果子类没有定义constructor
方法,那这个方法会被默认添加。也就是说,不管有没有显式定义,任何子类都有constructor
方法。