1. 原型链

此种继承方式的基本思想是:利用原型让一个引用类型继承另一个引用类型的属性和方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Parent() {
this.prop = true
}
Parent.prototype.getProp = function () {
return this.prop
}

function Son() {
this.subProp = false
}
Son.prototype = new Parent()
Son.prototype.getSubProp = function () {
return this.subProp
}
let instance = new Son
console.log(instance)

输出如下:

使用此方法实现要注意一点:若在重写原型链之前创建对象,则这个对象不会拥有后面重写原型链之后的方法和属性,我们看下一下下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Parent() {
this.prop = true
}
Parent.prototype.getProp = function () {
return this.prop
}

function Son() {
this.subProp = false
}
let s = new Son()

Son.prototype = new Parent()
Son.prototype.getSubProp = function () {
return this.subProp
}
let instance = new Son
console.log(s, '重写原型链之前')
console.log(instance, '重写原型链之后')

输出如下:

可以很明显的看到,重写原型链之前创建的对象s的原型并不会属性prop和方法getSubProp,所以,如果你要用此方法来实现继承,请务必在重写原型链之后再创建新对象。

原型链的问题:

(1)原型链实现继承,最主要的问题来自包含引用类型值的原型,请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent() {
this.colors = ['red', 'blue']
}

function Son() {
}

Son.prototype = new Parent()

let s1 = new Son()
s1.colors.push('green')
console.log(s1)

let s2 = new Son()
console.log(s2)

输出如下:

可以很明显的看到,第二次创建的实例s2的原型中的属性colors中也有了”green”。其实,引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是再原型对象中定义属性的原因。

(2)原型链的第二个问题是:再创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

基于以上两点,我们一般不建议使用原型链的方式实现继承,实际上大多数人也不会单独采用这种方式。

2. 借用构造函数

在解决原型中包含引用类型值带来问题的过程中,开发人员开始使用一种叫做“借用构造函数”的技术(有时候也叫伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数内部调用超类型构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Parent() {
this.list = ['football', 'baseball']
}

function Son() {
Parent.call(this)
}

let instance1 = new Son()
instance1.list.push('tennis')
console.log(instance1)

let instance2 = new Son()
console.log(instance2)

输入如下:

可以很明显的看到,实例中的引用类型值不再互相影响,由此解决原型链实现继承带来的引用类型值的问题。此外,相比原型链而言,借用构造函数还有一个很大的优势,即可以在子类型构造函数中向超类型构造函数传递参数。请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
function Parent(name) {
this.name = name
}

function Son() {
Parent.call(this, 'Tom')
this.age = 15
}

let instance = new Son()
console.log(instance)

输出如下:

这里我们要提醒各位的是,为了确保超类型构造函数不会重写子类型的属性,所以我们建议,请在调用超类型构造函数之后,再添加子类型中定义的属性。

借用构造函数的问题:

很明显的可以发现,这种模式实现的继承,子类型无法获得超类型的原型中定义的属性和方法。基于上述考虑,我们一般也很少单独使用此种方式实现继承。

3. 组合式继承

组合继承,有时候也叫伪经典继承,指的是通过将原型链和借用构造函数的技术组合在一起,取二者之所长实现继承的一种技术。其思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。由此,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有自己的属性。请看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function superType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
}
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.constructor = subType
subType.prototype.sayAge = _ => {console.log(this.age)}

let sub1 = new subType('Tom', 15)
console.log(sub1)

输出如下:

组合式继承的问题:

组合式继承虽然融合了原型链和借用构造函数两者的优势,解决了二者单独使用时带来的问题,但是此种方式也不是完美无缺的。我们观察上述输出,不难发现,此种方式实现继承的过程中,调用了两次超类型构造函数;一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。进而就出现了,就上述例子来说,可以发现超类型构造函数中的属性colors和name,既出现在子类实例属性中,也出现在子类原型链上。

4. 原型式继承

利用ES5新增的方法Object.create()实现原型式继承,这个方法接收两个参数:第一个参数用于作为新对象的原型,第二个参数(可选)用于为新对象定义额外的属性;其中,第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的属性描述符定义的。

1
2
3
4
5
6
let person = {
area: 'China',
list: ['football', 'baseball']
}
let p = Object.create(person)
console.log(p)

输入如下:

使用此种方式实现继承,所带来的问题和原型链方式一样,同样是引用类型值的问题。

5. 寄生式继承

此种方式的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象。请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function create(obj) {
let clone = Object.create(obj)
clone.sayName = function () {
console.log('Hello world')
}
return clone
}

let person = {
name: 'Tom',
list: ['baseball', 'football']
}
let p = create(person)
p.sayName() // Hello world

此种方式与构造函数模式类似,无法做到函数复用而降低效率。

6. 寄生组合式继承

所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。此种方式弥补了组合式继承方式带来的缺陷,请看下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function superType(name) {
this.name = name
this.colors = ['red', 'green', 'blue']
this.pi = function () {
console.log(123)
}
}
superType.prototype.sayName = function () {
console.log(this.name)
}
function subType(name, age) {
superType.call(this, name)
this.age = age
}

subType.prototype = Object.create(superType.prototype)
subType.prototype.constructor = subType
subType.prototype.sayAge = _ => {console.log(this.age)}

let sub1 = new subType('Tom', 15)
console.log(sub1)

输出如下:

可以很明显的看出,此种方式只调用一次超类型构造函数,因此避免了在原型链上创建多余的属性。

总结

业界普遍认为寄生组合式继承是引用类型最理想的继承模式。