原型链(Prototype chain)
ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承里一个引用类型的属性和方法。简单回顾下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,加入我们让原型对象等于另一个类型的实例,结果会怎样呢?显然,此时的原型独享将包含一个指向另一哥原型的指针,相应地,另一个原型中也包含着一个指向另一哥构造函数的指针。加入另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
确定原型和实例的关系
可以通过两种方式来确定原型和实例之间的关系。第一种方式是使用instanceof操作符,只要用这个操作符来测试实例与原型链中出现过的构造函数,就会返回true。第二种方式是使用isPrototypeOf()方法。同样,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
谨慎地定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
还要一定需要提醒读者,即在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。例如:
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; } someotherMethod:function(){ return false; } } var instance=new SubType(); alert(instance.getSuperValue());
以上代码展示了刚刚把SuperType的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个object的实例,而非SuperType的实例,因此我们设想中的原型链已经被切断--SuperType和SubType之间已经没有关系了。
原型链的问题
原型链虽然很强大,可以用它来实现继承,但它也存在一些问题。其中,最主要的问题来自包含引用类型值的原型。因为包含引用类型值的原型属性会被所有实例共享;而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。在同个原型来实现继承时,原型实际上胡变成另一个类型的实例。于是,原先的实例属性也就顺理成章地变成了现在的原型属性了。下面代码可以用来说明这个问题。
function SuperType(){ this.colors=[‘red‘,‘blue‘,‘green‘]; } function SubType(){} SubType.prototype=new SuperType(); var instance1=new SubType(); instance1.colors.push(‘black‘); alert(instance1.colors); //‘red‘,‘blue‘,‘green‘,‘black‘ var instance2=new SubType(); alert(instance2.colors); //‘red‘,‘blue‘,‘green‘,‘black‘
这个例子中的SuperType构造函数定义了一个colors属性,该属性包含一个数组,SuperType的每个势力都会有个字包含自己数组的colors属性。当subType通过原型链继承了SuperType之后,subType.prototype就变成了SuperType的一个实例,因此它也拥有了一个它自己的colors属性。但结果是什么呢?结果是subType的所有实例都会共享这一个colors属性。而我们队instance1.colors的修改能够通过instance2.colors反映出来,就已经证实了这一点。
原型链的第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,应该说是没有办法在不影响所有对象市里的情况下,给超类型的构造函数传递参数。有鉴于此,再加上前面刚哥讨论过的由于原型中包含引用类型值所带来的问题,实践中很少会单独使用原型链。
继承
所谓继承就是子类继承父类的特征与行为,使得子类对象具与父类相同的行为。但是javascript 是没有class、抽象类、接口等抽象概念,javascript 只有对象,那么js中是如何让对象与对象之间产生继承关系呢?
基于对象的继承
在原型链中说过,如果在对象上没有找到需要的属性或者方法引用,js引擎就会继续在内部属性[[prototype]] 指向的对象上进行查找。同理如果还是没有找到需要的引用,就会继续查找它的内部属性[[prototype]]指向的对象,以此类推,层层向上直到找到对象的原型为null为止,这一系列的链接称为原型链。所以在原型链中我才会js中的继承是基于原型实现;
组合继承
组合继承是比较常用的一种继承方法,思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。
function Person(name,age){ this.name=name; this.age=age; } Person.prototype.sayName=function(){ return this.name; }; //父亲 function Parent(work,country,name,age){ this.work=work; this.country=country; this.parentInfo=function(){ return ‘hello,我叫:‘ + this.name +‘ 我是一名:‘ + this.work + ‘,我来自:‘ + this.country; } Person.call(this,name,age);//父类型传参 第二次调用Person() }; Parent.prototype=new Person();//重写Parent的原型对象 第一次调用Person() var myParent=new Parent(‘manager‘,‘china‘,‘amy‘,23); myParent.sayName(); myParent.parentInfo(); //儿子 function Child(work,country,name,age,sex){ this.sex=sex; this.childInfo=function(){ return ‘hello,我叫:‘ + this.name + ‘ 我是一名:‘ + this.work + ‘,我来自‘ + this.country + ‘,我是一个可爱的 ‘ + this.sex; } Parent.call(this,work,country,name,age); } Child.prototype=new Parent();//重写Child的原型对象 var myBaby=new Child(‘child‘, ‘广州‘, ‘超级飞侠-多多‘, 3, ‘girl‘); myBaby.parentInfo() myBaby.childInfo()
这里基类是人,子类是父亲、儿子,这里通过重写原型对象以及在构造函数中调用父类的构造函数,并且用call改变了this 指针,从而达到儿子继承了父亲,父亲继承了人;
弊端:黄色字体的行中调用person()构造函数的代码。在第一次调用person()构造函数时,parent.prototype会得到两个属性:name和age;他们都是person的实例属性,只不过现在位于parent()的原型中。当调用parent()构造函数时,又会调用一次person()构造函数,这一次又在心对象上创建了实例属性name和age。于是,这两个属性就屏蔽了原型中的两个同名属性。
寄生组合式继承
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混合形式来继承方法。其背后的基本思路:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型的原型的一个副本而已。本质上,就是使用寄生式继承超类型的原型,然后再将结果指定给子类型的原型。
function inheritPrototype(subType,superType){ var prototype=object(superType.prototype);//创建对象 prototype.constructor=subType;//增强对象 SubType.prototype=prototype;//指定对象 }
在函数内部,第一步是创建超类型原型的一个副本。第二步是为了创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(副本)赋值给子类型的原型。然后把代码改写如下:
function Person(name,age){ this.name=name; this.age=age; } Person.prototype.sayName=function(){ return this.name; }; //父亲 function Parent(work,country,name,age){ this.work=work; this.country=country; this.parentInfo=function(){ return ‘hello,我叫:‘ + this.name +‘ 我是一名:‘ + this.work + ‘,我来自:‘ + this.country; } Person.call(this,name,age);//父类型传参 }; inheritPrototype(Parent , Person) var myParent=new Parent(‘manager‘,‘china‘,‘amy‘,23); myParent.sayName(); myParent.parentInfo(); //儿子 function Child(work,country,name,age,sex){ this.sex=sex; this.childInfo=function(){ return ‘hello,我叫:‘ + this.name + ‘ 我是一名:‘ + this.work + ‘,我来自‘ + this.country + ‘,我是一个可爱的 ‘ + this.sex; } Parent.call(this,work,country,name,age); } inheritPrototype(Child,Parent) var myBaby=new Child(‘child‘, ‘广州‘, ‘超级飞侠-多多‘, 3, ‘girl‘); myBaby.parentInfo() myBaby.childInfo()
总结:
1)对象之间的链接通过[[prototype]]进行关联
2)new 操作符关键点
3)prototype 是函数对象的属性,_proto_ 是实例的内部属性[[prototype]]
4)原型链是沿着_proto_链接进行查找
5)寄生组合式继承时实现基于原型继承的最有效方式
参考:《javascript高级程序设计(第3版)》