Chipmunk & Panda

-- 鼠熊部落格

All work and no play makes Jack a dull boy.

JavaScript 对象继承方法

原型链(存在引用值共享问题)、盗用构造函数(存在函数重用问题)、组合继承(存在父类构造函数重复调用问题)、原型式继承(存在引用值共享问题)、寄生式继承(存在函数重用问题)、寄生组合式继承(最推荐)。

类继承(ES6 语法糖)真香。

预备代码

定义一个父类,包括一个原始值属性和引用值属性,并在其原型上定义了一个输出函数。

1
2
3
4
5
6
7
function SuperType() {
this.prop1 = 9527
this.prop2 = [9, 5, 2, 7]
}
SuperType.prototype.printProp1 = function () {
console.log(this.prop1)
}

原型链继承

原型链继承思想即将父类的实例作为子类的原型,从而使得子类能够继承父类的属性和方法。

1
2
3
4
function SubType1() {}
SubType1.prototype = new SuperType()
let instance1 = new SubType1()
instance1.printProp1() // 9527

原型链的问题在于所有子类实例会共享作为原型的父类实例的引用值属性,即引用值共享问题。

1
2
3
4
console.log(instance1.prop2) // [ 9, 5, 2, 7 ]
let instance2 = new SubType1()
instance2.prop2.push(666)
console.log(instance1.prop2) // [ 9, 5, 2, 7, 666 ]

盗用构造函数

盗用构造函数方法用于解决引用值共享问题,其在子类构造函数中调用父类构造函数,使得每个子类实例都会自行创建一个新的引用值属性。

1
2
3
4
5
6
7
8
9
function SubType2() {
SuperType.call(this)
}
let instance3 = new SubType2()
let instance4 = new SubType2()
console.log(instance4.prop2) // [ 9, 5, 2, 7 ]
instance3.prop2.push(777)
console.log(instance3.prop2) // [ 9, 5, 2, 7, 777 ]
console.log(instance4.prop2) // [ 9, 5, 2, 7 ]

盗用构造函数方法的问题在于父类函数不能重用,必须在构造函数中定义方法,子类也不能访问父类原型上定义的方法。

1
instance3.printProp1() // TypeError: instance3.printProp1 is not a function

组合继承

组合继承方法综合了原型链方法和盗用构造函数方法,使用前者继承原型上的属性和方法,使用后者继承实例属性,从而同时解决了引用值共享和函数重用问题。

1
2
3
4
5
6
7
8
9
10
11
function SubType3() {
SuperType.call(this)
}
SubType3.prototype = new SuperType()
let instance5 = new SubType3()
let instance6 = new SubType3()
console.log(instance6.prop2) // [ 9, 5, 2, 7 ]
instance5.prop2.push(888)
console.log(instance5.prop2) // [ 9, 5, 2, 7, 888 ]
console.log(instance6.prop2) // [ 9, 5, 2, 7 ]
instance5.printProp1() // 9527

组合继承方法的问题在于父类构造函数会被调用两次,而实际上只有子类调用父类构造函数时才需要。

1
2
3
4
5
function SubType3() {
SuperType.call(this) // 第二次
}
SubType3.prototype = new SuperType() // 第一次,不需要
let instance5 = new SubType3()

寄生式组合继承

寄生式组合继承在组合继承的基础上解决了父类构造函数的重复调用问题,在介绍该方法之前需要了解原型式继承与寄生式继承。

原型式继承

原型式继承目的在于在不自定义类型的情况下通过原型实现对象之间的信息共享,Object.creat() 将这一概念规范化了。

1
2
3
4
5
6
7
8
let person = {
name: 'person9527',
number: [9, 5, 2, 7]
}
// 下面等效于let anotherPerson = Object.create(person)
function Person() {}
Person.prototype = person
let anotherPerson = new Person()

上述代码本质上是对 person 进行了一次浅复制,其问题与原型链继承相同,在于引用值共享问题。实际上,该方法与原型链继承的思想基本一致,区别不过在于原型链继承定义了一个父类并将其的一个实例作为子类的原型,原型式继承则是直接将一个已有对象作为子类的原型。

寄生式继承

寄生式继承就是在原型式继承基础上为子类实例添加了新方法。

1
2
3
anotherPerson.sayName = function () {
console.log(this.name)
}

该方法会导致函数难以重用。

寄生式组合继承

对于之前盗用了构造函数的 SubType3

1
2
3
function SubType3() {
SuperType.call(this)
}

为了解决函数重用问题,组合继承操作如下:

1
2
SubType3.prototype = new SuperType()
let instance5 = new SubType3() // 在此会重复调用父类构造函数

寄生式组合继承基本思路是不通过调用父类构造函数给子类原型赋值,而是取得父类原型的一个副本(Object.creat()),在此需要记得将副本的 constructor 改为子类构造函数,否则会依然是父类构造函数。

1
2
3
let prototype = Object.create(SuperType.prototype)
prototype.constructor = SubType3
SubType3.prototype = prototype

上述代码中 prototype 的构造等价于如下操作:

1
2
3
4
let prototype2 = {}
prototype2.__proto__ = SuperType.prototype
prototype2.constructor = SubType3
SubType3.prototype = prototype2

这里只调用了一次 SuperType 构造函数,避免了 SubType.prototype 上不必要也用不到的属性,因此可以说这个例子的效率更高。而且,原型链仍然保持不变,因此 instanceof 操作符和 isPrototypeOf() 方法正常有效。寄生式组合继承可以算是引用类型继承的最佳模式。(引自《JavaScript 高级程序设计》)

类继承

ES6 新引入了 类(class) 的概念,直接对继承问题进行了最佳实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SuperType {
constructor() {
this.prop1 = 9527
this.prop2 = [9, 5, 2, 7]

console.log('SuperType constructor.')
}

printProp1() {
console.log(this.prop1)
}
}

class SubType extends SuperType {}

let instanceA = new SubType() // SuperType constructor.
let instanceB = new SubType() // SuperType constructor.
console.log(instanceB.prop2) // [ 9, 5, 2, 7 ]
instanceA.prop2.push(999)
console.log(instanceA.prop2) // [ 9, 5, 2, 7, 999 ]
console.log(instanceB.prop2) // [ 9, 5, 2, 7 ]
instanceA.printProp1() // 9527

类其实属于 ES6 的语法糖,虽然表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的仍让是原型和构造函数概念。