0%

JS继承的实现方式及比较

继承是面向对象语言中的重要概念,许多面向对象的语言都支持类的继承。本文介绍几种 JavaScript 中常用的继承实现方法以及各自的特点。

1. 简单的原型继承

1
2
3
4
5
6
7
8
9
10
function SuperType() {
this.name = "super"
}
function SubType() {}

// 利用原型链实现继承
SubType.prototype = new SuperType()

var instance1 = new SubType()
console.log(instance1.name) // super

简单的原型继承存在以下两个问题:

  • 包含引用类型值的原型属性会被所有实例共享,在通过原型来实现继承时,原型实际上也会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性。思考一下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function SuperType() {
    this.names = ["sillywa", "xinda"]
    }
    function SubType() {}

    // 利用原型链实现继承
    SubType.prototype = new SuperType()

    var instance1 = new SubType()
    instance1.names.push("hahah")
    console.log(instance1.names) // ["sillywa", "xinda", "hahah"]

    var instance2 = new SubType()
    console.log(instance2.names) // ["sillywa", "xinda", "hahah"]

    这个例子中,SuperType构造函数定义了一个 names 属性,该属性为一个数组(引用类型)。SuperType的每个实例都会有自己的 names 属性。当 SubType 通过原型链继承了 SuperType 之后,SubType.prototype 就变成了 SuperType 的一个实例,因此它也拥有自己的 names 属性——就跟专门创建了一个 SubType.prototype.names 属性一样。但是结果就是 SubType 的所有实例共享一个 names 属性。

  • 简单的原型继承的另一个问题是:在创建子类类型的实例时,不能向超类类型的构造函数中传递参数。

因此在继承上我们经常不会单独使用原型继承。

2. 借用构造函数继承(经典继承)

这种继承的思想是在子类的构造函数内部调用超类的构造函数,该方法使用 call() 和 apply() 方法在新创建的对象上执行构造函数。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(age, name) {
this.colors = ["blue", "red"]
this.age = age
this.name = name
}
function SubType() {
SuperType.call(this, ...arguments)
}

var instance1 = new SubType(23, "sillywa")
instance1.colors.push("yellow")
console.log(instance1.colors, instance1.name)

var instance2 = new SubType(12, "xinda")
console.log(instance2.colors, instance2.name)

借用构造函数继承也有一些缺点,比如方法都只能在构造函数中定义,没有办法实现方法的复用。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(name) {
this.name = name
this.sayName = function() {
return this.name
}
}
function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}

// 每次实例化一个对象,都会重新实例化 sayName 方法
var instance1 = new SubType("sillywa", 24)
console.log(instance1)
console.log(instance1.sayName())

3. 组合式继承

组合继承结合了原型继承和借用构造函数继承的优点,其背后的思想是,使用原型链实现对原型方法的继承,使用构造函数实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的服用,又通过构造函数实现了每个实例都有自己的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function SuperType(name) {
this.name = name
this.colors = ["red", "yellow"]
}
// 方法写在原型上
SuperType.prototype.sayName = function() {
return this.name
}
function SubType(name, age) {
// 通过 构造函数继承属性
SuperType.call(this, name)
this.age = age
}
// 通过原型继承方法
SubType.prototype = new SuperType()

// 重写了 SubType 的 prototype 属性,因此其 constructor 也被重写了,需要手动修正
SubType.prototype.constructor = SubType

// 定义子类自己的方法
SubType.prototype.sayAge = function() {
return this.age
}

测试案例:

1
2
3
4
5
6
7
8
9
10
var instance1 = new SubType("sillywa", 23)
instance1.colors.push("blue")
console.log(instance1.colors) //["red", "yellow", "blue"]
console.log(instance1.sayName()) // sillywa
console.log(instance1.sayAge()) // 23

var instance2 = new SubType("xinda", 90)
console.log(instance2.colors) // ["red", "yellow"]
console.log(instance2.sayName()) // xinda
console.log(instance2.sayAge()) // 90

组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 JavaScript 中最常用的继承模式。

4. 原型式继承

借助原型可以通过已有的对象创建新对象,同时还不必因此创建自定义类型。为达到这个目的,可以定义如下函数:

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

在 object 函数内部,首先创建了一个临时性构造函数 F,将 F 的 prototype 属性指向传入的对象 o,并返回 F 的一个实例,则该实例继承 o 的所有属性和方法。从本质上讲,create() 对传入的对象执行了一次浅复制。看以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
var person = {
name: "sillywa",
firends: ["Johe"]
}

var person1 = create(person)
person1.name = "coder"
person1.firends.push("Kobe")

var person2 = create(person)
person2.firends.push("Cury")
console.log(person2.firends) // ["Johe", "Kobe", "Cury"]

ES5 通过新增 Object.create() 方法规范化了原型式继承。这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create() 与 create() 方法的行为相同。

Object.create() 方法的第二个参数与 Object.defineProterties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:

1
2
3
4
5
6
7
8
9
var person = {
name: "sillywa"
}
var person1 = Object.create(person, {
name: {
value: "John"
}
})
console.log(person1.name) // John

5. 寄生式继承

寄生式继承的思路与继承构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真正地是它做了所有工作一样返回对象。以下是寄生式继承的代码:

1
2
3
4
5
6
7
function createAnother(original) {
var clone = Object.create(original)
clone.sayHi = function() {
console.log("Hi")
}
return clone
}

6. 组合寄生式继承

前面说过,组合继承是 JavaScript 最常用的继承模式,不过它也有自己的缺点,组合继承最大的问题是,无论什么情况下都会调用两次超类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name) {
this.name = name
this.colors = []
}
SuperType.prototype.sayName = function() {
return this.name
}

function SubType(name, age) {
// 第一次调用父类的构造函数
SuperType.call(this,name)
this.age = age
}
// 第二次调用父类的构造函数
SubType.prototype = new SuperType()
SubType.prototype.constructor = SubType
SubType.prototype.sayAge = function() {
return this.age
}

组合寄生式继承就是为了解决这一问题,将第二次调用构造函数改为使用 Object.create() 函数来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType(name) {
this.name = name
this.colors = []
}
SuperType.prototype.sayName = function() {
return 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 = function() {
return this.age
}