6.3 继承
实现继承一般有两种方法:接口继承和实现继承。接口继承只继承方法签名,而实现继承则继承实际的方法。ES里面没有重载,函数没有签名,所以只支持实现继承。
1. 原型链
概念:每个构造函数都有一个原型对象,这个对象包含一个指向构造函数的指针,而实例都包含一个指向原型对象内部的指针。假如让原型对象等于另一个类型的实例,此时原型对象将包含指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么
1.1 实现原型的一种基本模式:
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
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //true, 搜索实例》搜索SubType.prototype》搜索SuperType.prototype
console.log(SubType.prototype.property); //true
console.log(instance.constructor); //SuperType
console.log(SubType.prototype.constructor); //SubType的原型指向SuperType的原型,而这个原型对象的constructor属性指向SuperType
console.log(instance.toString()); //toString()方法来自由实例SuperType继承的Object.prototype.toString()。所有函数的默认原型都是Object的实例。
//用instanceof操作符来确定实例之间的关系。只要是原型链中出现过的构造函数,结果就会返回true。
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SubType); //true
//用isPrototypeOf()方法来检测原型链中出现过的原型。
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true
1.2 子类会覆盖超类型中的方法
子类型有时候需要覆盖超类型中的方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型之后。(这样才会生效)。同时也不能使用对象字面量创建原型方法,因为这样做会重写原型链。
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
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了SuperType
SubType.prototype = new SuperType();
//使用字面量添加新方法,会使上一行代码无效
SuperType.prototype = {
getSubValue: function(){
return this.subproperty;
},
someOtherMethod: function(){
return false;
}
};
var instance = new SubType();
console.log(instance.getSuperType); //undefined
1.3 原型链的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//前面介绍过包含引用类型值的原型属性会被所有的实例共享;而这正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
//通过原型来继承实现时,原型实际上会变成另一个类型的实例。于是,原先的实例属性也就变成了现在的原型属性了。
function SuperType(){
this.colors = ['red', 'blue', 'green'];
}
function SubType(){
}
//继承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
instance1.colors.push('black'); //来自prototype的colors
console.log(instance1.colors); //'red', 'blue', 'green', 'black'
var instance2 = new SubType();
console.log(instance2.colors); //'red', 'blue', 'green', 'black'
//SuperType构造函数定义了一个colors属性,该属性包含一个数组(引用类型值)。SuperType的每一个实例都会有各自包含数组的colors属性。当SubType通过原型链继承了SuperType之后,SubType.prototype就变成了SuperType的一个实例,因此拥有了一个colors属性--结果SubType所有的实例都会共享这一个colors属性(这个属性是在原型对象上的。搜索过程:实例本省=>原型对象)。所以对instance1.colors的修改会反映到instance2.colors,因为他们指向的是同一个地址。
//原型链的第二个问题是:不能在不影响所有对象实例的情况下,给超类型的构造函数传递参数。
2. 借用构造函数
在解决原型中包含引用类型值所带来的问题的过程中,开发人员开始使用一种叫做“借用构造函数”(constructor stealing)的技术(有时叫伪造对象或经典继承)。这种技术是在子类型构造函数的内部调用超类型构造函数。构造函数是在特定环境中执行代码的对象,因此通过使用apply()或者call()方法也可以在(将来)新创建的对象上执行构造函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function SuperType(){
this.colors = ['red','blue','green'];
}
function SubType(){
//继承了SuperType
SuperType.call(this);
}
var instance1 = new SubType();
instance1.colors.push('black');
console.log(!instance1.hasOwnProperty('colors') && ('colors' in instance1)); //false, 检测colors是否在prototype上
console.log(instance1.colors); //'red', 'blue', 'green', 'black', 操作instance1自身的属性colors
var instance2 = new SubType();
console.log(instance2.colors); //'red', 'blue', 'green'
构造函数的一个优势是可以在子类型构造函数中向超类型构造函数传递参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function SuperType(name){
this.name = name;
}
function SubType(){
//继承了SuperType,同时还传递了参数
SuperType.call(this, 'Nicholas');
//实例属性
this.age = 20;
}
var instance1 = new SubType();
console.log(instance1.name);
console.log(instance1.age);
//在SubType构造函数内部调用SuperType构造函数时,实际上是为SubType的实例设置了name属性。为了确保SuperType构造函数不会重写子类型的属性,可以在调用超类型构造函数之后,再添加应该在子类型中定义的属性。
借用构造函数的问题:方法都在构造函数中定义,函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对于子类型而言也是不可见的。结果所有类型都只能使用构造函数模式。所以此模式也是比较少单独使用。
3. 组合继承
组合继承(combination inheritance),也叫经典继承,是将原型链和借用构造函数组合到一起,从而发挥二者之长的继承模式。 其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
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
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
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 = function(){
console.log(this.age)
};
var instance1 = new SubType('Nicholas', 29);
instance1.colors.push('black');
console.log(instance1.colors); //'red,blue,green,black'
instance1.sayName(); //'Nicholas'
instance1.sayAge(); //29
var instance2 = new SubType('Greg', 27);
console.log(instance2.colors); //'red,blue,green'
instance2.sayName(); //'Greg'
instance2.sayAge(); //27
//两个不同的SubType实例instance1和instance2分别拥有自己的属性name和colors以及age,又可以使用相同的方法sayName和sayAge。
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为javascript中最常用的继承模式。而且instanceof和isPrototypeOf()也能够用于识别基于组合继承创建的对象。
4. 原型式继承
Douglas Crockford在2006年写了名为《Prototypal Inheritance in JavaScript》的文章,提出原型式继承。 这种方法没有使用严格意义上的构造函数,而是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
1
2
3
4
5
6
function object(o){
function F(){}
F.prototype = o;
return new F();
}
//在object()函数内部,先创建一个临时性的构造函数,然后将传入对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function object(o){
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: 'Nicholas',
friends: ['Shelby','Court','Van']
};
var anotherPerson = object(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = object(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); //'Shelby, Court, Van, Rob, Barbie'
//Douglas Crockford主张的这种模式要求必须有一个对象作为另一个对象的基础。object返回一个以person为原型的新对象,而原型person里面有一个基本类型和一个引用类型值属性。这意味着person.friends不仅属于person所有,而且会被所有object()生成的新对象(示例中是anotherPerson 和 yetAnotherPerson)共享。
ES5通过新增了Object.create()方法规范化了原型式继承(膜拜老道)。这个方法接受两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()和object()的行为相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
var person = {
name: 'Nicholas',
friends: ['Shelby','Court','Van']
};
var anotherPerson = Object.create(person);
anotherPerson.name = 'Greg';
anotherPerson.friends.push('Rob');
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = 'Linda';
yetAnotherPerson.friends.push('Barbie');
console.log(person.friends); //'Shelby, Court, Van, Rob, Barbie'
1
2
3
4
5
6
7
8
9
10
11
12
//Object.create()方法和Object.defineProperties()方法的第二个参数相同:每一个属性都是通过自己的描述符定义的。以这种方法指定的任何属性都会覆盖原型对象上的同名属性。
var person = {
name: 'Nicholas',
friends: ['Shelby','Court','Van']
};
var anotherPerson = Object.create(person, {
name:{
value: 'Greg'
}
});
console.log(anotherPerson.name); //'Greg'
在没有必要创建构造函数的情况下,想要一个对象和另一个对象保持类似,原型式继承完全可以胜任。
5. 寄生式继承
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对,最后再像真地是它做了所有工作一样返回对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log('Hi');
};
return clone;
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //'Hi'
//例子中的代码基于person对象返回了一个新对象,这个对象不仅具有person的所有属性和方法,还有自己的sayHi()方法。
6. 寄生组合模式继承
前面讲到组合模式是js最常见的继承模式,但是它有一个问题就是无论在什么情况下,都会调用超类型构造函数两次:一次是在创建子类型原型的时候,一次是在子类型构造函数内部。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function SuperType(name){
this.name = name;
this.colors = ['red','blue','green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name, age){
//第二次调用SuperType()
SuperType.call(this, name);
this.age = age;
}
//第一次调用SuperType()
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age)
};
//第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors;它们都是SuperType的实例属性。只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是这两个属性就屏蔽了原型中的两个同名属性。
所谓寄生组合式继承,既通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。其背后的思路是,不必为了指定类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
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
35
36
37
38
39
40
41
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){
//创建对象:超类型的副本prototype
var prototype = object(superType.prototype);
//增强对象:为副本添加constructor属性,从而弥补因即将重写原型而失去的默认的constructor属性
prototype.constructor = subType;
//指定对象:将新创建的副本赋值给子类型的原型
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function(){
console.log(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
console.log(this.age);
}
var person = new SubType('Jeff', 29);
person.sayName();
person.sayAge();
console.log(person.colors);
这个例子的高效在于它只调用了一次SuperType构造函数,并且因此避免了再SubType.prototype上面创建不必要的、多余的属性,同时原型链还能保持不变。
【本文内容摘自:《JavaScript高级程序设计》(第3版)Nicholas C.Zakas 著 李松峰 曹力 译】