继承
许多OO语言都支持两种继承方式:
- 接口继承:只继承方法签名。
- 实现继承: 继承实际的方法。
如前所述,由于ECMAScript函数没有签名,因此无法实现接口继承。
ECMAScript只支持实现继承,而且其实现继承主要依靠原型链来实现的。
6.3.1 原型链
ECMAScript将原型链作为继承的主要方法,其基本思想是:
通过原型让一个引用类型继承另一个引用类型的属性和方法。
简单的回顾一下实原型、构造函数、实例之间的关系:
- 每个构造函数都有一个prototype指针指向了原型对象
- 每个原型对象又有一个constructor属性指向了构造函数
- 每个实例有一个内部指针[[prototype]]指针指向了原型对象
如果我们让原型对象等于一个引用类型的实例,会发生什么?
SubType.prototype = new SuperType();
SubType.prototype其实就是SuperType的实例,这样它就有一个内部指针指向了SuperType的原型对象。
同样SuperType的原型对象也有一个constructor属性指向了SuperType构造函数。
这个原型对象有一个内部指针就会指向了另一个构造函数的原型对象,同样另一个构造函数的原型对象也有一个constructor属性指向了另一个构造函数,如果另一个原型对象又等于另一个构造函数的实例,那么上述关系同样成立,这样层层递进,就形成了实例和原型的链条。这就是所谓原型链的基本概念。
实现原型链有一种模式,其代码大致如下:
function SuperType(){
this.property = '超类';
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(){
this.subproperty = '子类';
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType();
console.log(instance.getSuperValue()); //'超类'
上面的例子改写一下,把参数传入构造函数中。
unction SuperType(name){
this.property = name;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(name){
this.subproperty = name;
}
//继承了SuperType
SubType.prototype = new SuperType('超类');
SubType.prototype.getSubValue = function(){
return this.subproperty;
}
var instance = new SubType('子类');
console.log(instance.getSuperValue()); //'超类'
实现的本质是:重写原型对象。
换句话说:原来存在SuperType实例中所有的属性和方法,现在也存在SubType.prototype中。因为SubType.prototype相当于SuperType的实例。
在上面的代码中,SubType并没有使用自己的原型,而是给它换了一个新原型。这个新原型就是SuperType的实例。这个新原型不仅具有作为SuperType实例所具有的属性和方法,而且其内部还有一个指针,指向了SuperType的原型。
最终结果就是这样的:
instance指向了SubType的原型。SubType又指向了SuperType的原型。
此外要注意:
instance.constructor指向了 SuperType,这是因为SuperType的原型指向了另一个对象——SuperType的原型,而这个原型对象的constructor属性指向了SuperType构造函数。
1.别忘记默认的原型
2.确定原型和实例之间的关系
3.谨慎定义方法
通过原型链实现继承时,不能使用对象字面量创建原型方法,因为这样做会重写原型链。
function SuperType(name){
this.property = name;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
}
function SubType(name){
this.subproperty = name;
}
//继承了SuperType
SubType.prototype = new SuperType('超类');
SubType.prototype = {
getSubValue: function(){
return this.subproperty;
}
}
var instance = new SubType('子类');
console.log(instance.getSuperValue()); //TypeError: instance.getSuperValue is not a function
4.原型链的问题
6.3.2 借用构造函数
6.3.3 组合继承
组合继承是集于构造函数和原型链的优点。思路是:
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
6.3.4 原型式继承
道格拉斯 克罗克福德在一遍文章中介绍了一种实现继承的方法,这种方法并没有使用严格意义上的构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o){
//创建临时的构造函数
function F(){}
F.prototype = o;
return new F();
}
在object函数内部,先创建一个临时构造函数,然后传入的对象作为这个临时构造函数的原型,最后返回了这个临时构造函数的一个新实例。从本质上讲,Object对传入的对象进行了一次浅复制。
function object(o){
//创建一个临时的构造函数F
function F(){}
F.prototype = o;
return new F();
}
var person = {
name: 'xiao',
colors: ['yellow','blue']
}
var anotherPerson = new Object(person);
console.log(anotherPerson);
anotherPerson.name = 'tianxia';
anotherPerson.colors.push('green');
console.log(anotherPerson);
var yetAnotherPerson = new Object(person);
console.log(yetAnotherPerson);
yetAnotherPerson.colors.push('red');
console.log(yetAnotherPerson);
console.log(person);
Object.create()规范化了原型式的继承,这个方法接受两个参数:
- 一个用作新对象原型的对象
- 一个为新对象定义额外属性的对象。
var anotherPerson1 = Object.create(person);
console.log(anotherPerson1);
anotherPerson1.name = '运动有利于健身';
anotherPerson1.colors.push('red');
console.log(anotherPerson1);
var yetAnotherPerson1 = Object.create(person);
console.log(yetAnotherPerson1);
yetAnotherPerson1.name = '以后提倡运动';
yetAnotherPerson1.colors.push('purple');
console.log(yetAnotherPerson1);
Object.create()的第二个参数与Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
var anotherPerson = Object.create(person,{
name: {
value:'运动'
}
});
console.log(anotherPerson.name); //运动
提问:
(1)这里临时构造函数F的意义在哪?
为后续返回新的对象实例做铺垫,我们这里借用的是一个临时构造函数的原型,其实就是copy了一份对象。
原型式继承的缺点:
引用类型值的属性始终都会共享相应的值,正如原型模式的问题一样。
6.3.5 寄生式继承
思想:创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnOther(original){
//通过调用函数创建一个新对象
var clone = object(original);
//以某种方式来增强对象
clone.sayHi = function(){
console.log('Hi');
}
return clone; //返回这个对象
}
var person = {
name: 'tianxia6',
color: ['red']
}
var other = createAnother(person);
console.log(other);
other.name = '我需要你的强大';
console.log(other);
示例:
6.3.6 寄生组合继承
前面说过,继承组合继承是JavaScript最常用的继承模式;不过它也有自己的不足。组合继承最大的问题就是无论在任何情况下都会调用两次超类型的构造函数:
- 一次是创建子类型原型的时候
SubType.prototype = new SuperType()
- 另一次在子类型的构造函数内部。
function SubType(){
SuperType.call(this);
}
没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重新写这些属性。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(SubType,SuperType){
var prototype = object(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = ['red']
}
SuperType.prototype.sayName = function(){
console.log(this.name);
}
function SubType(name,age){
//继承属性
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType,SuperType);
var instance1 = new SubType('tianxia123',123);
console.log(instance1);
instance1.colors.push('white');
console.log(instance1.colors);
var instance2 = new SubType('xiao456',456);
instance2.colors.push('#000');
console.log(instance2.colors);