继承
- 许多 OO 语言都支持两种继承方式:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法
- 如前所述,由于函数没有签名,在 ECMAScript 中无法实现接口继承。ECMAScript 只支持实现继承,而且其实现继承主要是依靠原型链来实现的
原型链
- ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。
- 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例包含一个指向原型对象的内部指针。
- 如果让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含着一个指向另一个构造函数的指针
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1function SuperType () {
2 this.property = true
3}
4
5SuperType.prototype.getSuperValue = function () {
6 return this.property
7}
8
9function SubType () {
10 this.subproperty = false
11}
12
13// 继承了 SuperType
14SubType.prototype = new SuperType()
15
16SubType.prototype.getSubValue = function () {
17 return this.subproperty
18}
19
20var instance = new SubType()
21alert(instance.getSuperValue()) // true
22
23
- 别忘记默认的原型
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系
-
instanceof
-
只要用这个操作符来测试实例与原型链中出现过的构造函数,结果就会返回 true
1
2
3
4
5 1alert(instance instanceof Object) // true
2alert(instance instanceof SuperType) // true
3alert(instance instanceof SubType) // true
4
5
-
isPropertypeOf
-
只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型,因此 isPropertyOf() 方法也会返回 true
1
2
3
4
5 1alert(Object.prototype.isPropertypeOf(instance)) // true
2alert(SuperType.prototype.isPropertypeOf(instance)) // true
3alert(SubType.prototype.isPropertypeOf(instance)) // true
4
5
原型链的问题
- 包含引用类型值得原型属性会被所有实例共享
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数
借用构造函数
在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数(constructor stealing)的技术(有时候也叫做伪造对象或经典继承)。
-
思想即是在子类型构造函数的内部调用超类型的构造函数。
-
因为构造函数也是函数,函数只不过是在特定环境中执行代码的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1function SuperType () {
2 this.colors = ['red', 'blue', 'green']
3}
4
5function SubType () {
6 // 继承了 SuperType
7 SuperType.call(this)
8}
9
10var instance = new SubType()
11instance.colors.push('black')
12alert(instance1.colors) // red, blue, green, black
13
14var instance2 = new SubType()
15alert(instance2.colors) // red, blue, green
16
17
传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13 1function SuperType (name) {
2 this.name = name
3}
4
5function SubType () {
6 // 继承了 SuperType,同时海传递了参数
7 SuperType.call(this, 'Nicholas')
8
9 // 实例属性
10 this.age = 29
11}
12
13
借用构造函数的问题
如果仅仅是借用构造函数,那么也将无法避免构造函数模式存在的问题——方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。借用构造函数的技术也是很少单独使用的。
组合继承(combination inheritence)
-
有时候也叫做伪经典继承
-
指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式
-
思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承
-
这样,既通过在原型上定义方法实现了函数复用,又能保证每个实例都有它自己的属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34 1function SuperType (name) {
2 this.name = name
3 this.colors = ['red', 'blue', 'green']
4}
5
6SuperType.prototype.syaName = function () {
7 alert(this.name)
8}
9
10function SubType (name, age) {
11 // 继承属性
12 SuperType.call(this, name)
13 this.age = age
14}
15
16// 继承方法
17SubType.prototype = new SuperType()
18
19SubType.prototype.sayAge = function () {
20 alert(this.age)
21}
22
23var instance1 = new SubType('Nicholas', 29)
24instance1.colors.push('black')
25alert(instance1.colors) // "red, blue, green, black"
26instance1.sayName() // "Nicholas"
27instance1.sayAge() //29
28
29var instance2 = new SubType('Greg', 27)
30alert(instance2.colors) // "red, blue, green"
31instance2.sayName() // "Greg"
32instance2.sayAge() // 27
33
34
- 组合模式避免了原型链的借用构造函数的缺陷,融合了它们的优点,称为 JavaScript 中最常用的继承模式。
- 而且,instanceof 和 isPrototypeOf() 也能够识别基于组合继承创建的对象
原型式继承
-
道格拉斯·克劳克福德在 2006 年介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数
-
他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型
-
给出了如下函数
1
2
3
4
5
6
7 1function object (o) {
2 function F () {}
3 F.prototype = o
4 return new F()
5}
6
7
- 从本质上讲,object() 对传入其中的对象执行了一次潜复制
例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 1var person = {
2 name: 'Nicholas',
3 friends: ['Shelby', 'Court', 'Van']
4}
5
6var anotherPerson = object(person)
7anotherPerson.name = 'Greg'
8anotherPerson.friends.push('Rob')
9
10var yetAnotherPerson = object(person)
11yetAnotherPerson.name = 'Linda'
12yetAnotherPerson.friends.pushj('Barbie')
13
14alert(person.friends) // "Shelby, Court, Van, Rob, Barbie"
15
16
- 克罗克福德主张的这种原型式继承,要求你必须有一个对象可以作为另一个对象的基础。
- 如果有这个一个对象的话,可以把它传递给 object() 函数,然后再根据具体需求对得到的对象加以修改即可
- 实际上,这就相当于又创建了 person 对象的两个副本
object.create()
-
这个方法规范化了原型式的继承,这个方法接收两个参数:一个用作新对戏原型的对象和(可选的)一个为新对象定义额外属性的对象。
-
在传入一个参数的情况下,Object.create() 与 object() 方法的行为相同
-
第二个参数与 Object.defineProperties() 方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如“
1
2
3
4
5
6
7
8
9
10
11
12
13
14 1var person = {
2 name: 'Nicholas',
3 friends: ['Shelby', 'Court', 'Van']
4}
5
6var anotherPerson = Object.create(person, {
7 name: {
8 value: "Greg"
9 }
10})
11
12alert(anotherPerson.name) // "Greg"
13
14
寄生式继承
-
思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真的是它做了所有工作一样返回对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1function createAnother (original) {
2 var clone = object(original)
3 clone.sayHi = function () {
4 alert('hi')
5 }
6 return clone
7}
8
9var person = {
10 name: 'Nicholas',
11 friends: ['Shelby', 'Court', 'Van']
12}
13
14var anotherPerson = createAnother(person)
15anotherPerson.sayHi()
16
17
- 这个例子中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi() 方法
- 在主要考虑对象而不是自定义类型的构造函数的情况下,寄生式继承也是一种有用的模式。前面示范继承模式时使用的 object() 函数不是必需的,任何能够返回新对象的函数都适用于此模式
- 这一模式不能做到函数复用
寄生组合式继承
组合继承的不足
-
无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
-
子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1function SuperType (name) {
2 this.name = name
3 this.colors = ["red", "blue", "green"]
4}
5
6SuperType.prototype.sayName = function () {
7 alert(this.name)
8}
9
10function SubType (name, age) {
11 SuperType.call(this, name) // 第二次调用 SuperType()
12 this.age = age
13}
14
15SubType.prototype = new SuperType() // 第一次调用 SuperType()
16SubType.prototype.constructor = SubType
17SubType.prototype.sayAge = function () {
18 alert(this.age)
19}
20
21var instance = new SubType('Nicholas', 29)
22
23
- 当调用 SubType 构造函数时,又会调用一次 SuperType 的构造函数,这一次又在新对象上创建了实例属性 name 和 colors 。于是,这两个属性就屏蔽了原型中的两个同名属性。
如图:
-
寄生组合式继承即可解决这个问题
-
寄生组合式继承, 即通过构造函数来继承属性,通过原型链的混成形式来继承方法。
-
思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
-
本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
-
基本模式如下:
1
2
3
4
5
6
7 1function inheritPrototype (subType, superType) {
2 var prototype = object(superType.prototype) // 创建对象
3 prototype.constructor = subType // 增强对象
4 subType.prototype = prototype // 指定指定对象
5}
6
7
-
这样,我们就可以调用 inheritPrototype() 函数的语句,去替换前面例子中为子类型原型赋值的语句了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 1function SuperType (name) {
2 this.name = name
3 this.colors = ['red', 'blue', 'green']
4}
5
6SuperType.prototype.sayName = function () {
7 alert(this.name)
8}
9
10function SubType (name, age) {
11 SuperType.call(this, name)
12 this.age = age
13}
14
15inheritPrototype(SubType, SuperType)
16
17SubType.prototype.sayAge = function () {
18 alert(this.age)
19}
20
21
- 这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType.prototype 上面创建不必要的、多余的属性。于此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf() 。