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用intends继承了Person。这里需要注意的是,子类在继承父类的时候必须先在自己的构造函数(constructor)里调用super方法,而且要写在this之前,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

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

0%