Minakata的博客

世界上没有游戏制造机,有的只是艰辛的劳动。
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

javascript继承技术步步深入

Posted on 2011-10-07 10:44  Minakata  阅读(1328)  评论(1编辑  收藏  举报

1、构造函数模式

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}

var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",27,"Doctor");

person1.sayName();//"Nicholas"
person2.sayName();//"Greg"

alert(person1.constructor == Person);//true
alert(person2.constructor == Person);//true

alert(person1 instanceof Object);//true
alert(person1 instanceof Person);//true
alert(person2 instanceof Object);//true
alert(person2 instanceof Person);//true

问题:

每个方法都要在每个实例上重新创建一遍。在前面的例子中,person1和person2都有一个名为sayName()的方法,但那两个方法不是同一个Function的实例。

alert(person1.sayName == person2.sayName);//false

然而,创建两个完成同样任务的Function实例的确没有必要;况且有this对象在,根本不用在执行代码前就把函数绑定到特定对象上面。因此,可以通过把函数定义转移到构造函数外部来解决这个问题:

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}

function sayName(){
alert(this.name);
}

var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",27,"Doctor");

alert(person1.sayName == person2.sayName);//true

问题:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。更让人无法接受的是:如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。

2、原型模式

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};

var person1 = new Person();
person1.sayName();//"Nicholas"
var person2 = new Person();
person2.sayName();//"Nicholas"

alert(person1.sayName == person2.sayName);//true

理解原型:无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。在默认情况下,所有prototype属性都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针。Person.prototype.constructor指向Person。创建了自定义的构造函数之后,其原型属性默认只会取得constructor属性;至于其他方法,则都是从object继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型属性。在很多实现中,这个内部属性的名字是_proto_。不过,要明确的真正重要一点,就是这个链接存在于实例与构造函数的原型属性之间,而不是存在于实例与构造函数之间。

alert(Person.prototype.isPrototypeOf(person1));//true
alert(Person.prototype.isPrototypeOf(person2));//true

每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果在实例中添加一个与原型中属性同名的属性,则该属性会屏蔽原型中的那个属性。添加的同名属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为null,也只会在实例中设置这个属性,而不会恢复其指向原型的链接。不过,使用delete操作符则可以完全删除实例属性,从而让我们嫩巩固重新访问原型中的属性。in操作符只要通过对象能访问到属性就返回true。hasOwnProperty()只在属性存在于实例中时才返回true。

function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

alert(person1.hasOwnProperty("name"));//false
alert("name" in person1);//true

person1.name = "Greg";
alert(person1.name);//"Greg"
alert(person1.hasOwnProperty("name"));//true
alert("name" in person1);//true

delete person1.name;
alert(person1.name);//"Nicholas"
alert(person1.hasOwnProperty("name"));//false
alert("name" in person1);//true

更简单的原型语法:

Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function(){
alert(this.name);
}
};

在上面代码中,我们将Person.prototype设置为等于一个以对象字面量形式创建的新对象。本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的属性指向Object构造函数,不再指向Person函数。尽管instanceof操作符还能返回正确的结果,但是constructor已经无法确定对象的类型了。如果constructor的值真的很重要,可以特意设置回适当的值:

Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function(){
alert(this.name);
}
};

由于在原型中查找值的过程是一次搜索,因此我们对原型对象所作的任何修改都能够立即从实例上反映出来,即使是先创建了实例后修改原型也照样如此。比如新定义一个person.sayHi();,其原因可以归结为实例与原型之间松散连接的关系。但如果是重写整个原型对象,那情况就不一样。我们知道,调用构造函数时会为实例添加一个指向最初原型的_proto_指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

原型对象的问题:首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。这还不是最大的问题,最大的问题是由其共享的本性所导致的。对于包含引用类型值得属性来说,就会出现严重问题。

function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby","Court"],
sayName : function(){
alert(this.name);
}
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push("Van");

alert(person1.friends);//"Shelby,Court,Van"
alert(person2.friends);//"Shelby,Court,Van"
alert(person1.friends === person2.friends);//true

3、组合使用构造函数模式和原型模式

构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性和副本,但同时又共享这对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数,可谓是集两种模式之长。这种模式是目前在ECMAScript中使用最广泛、认同度最高的一种创建自定义类型的方法。

function Person(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby","Court"];
}

Person.prototype = [
constructor : Person,
sayName : function(){
alert(this.name);
}
}

var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",29,"Doctor");

person1.friends.push("Van");
alert(person1.friends === person2.friends);//false
alert(person1.sayName === person2.sayName);//true


4、原型链

原型链:ECMAScript中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。简单回顾一下构造函数、原型、实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

function SuperType(){
this.property = true;
}

SuperType.prototype.getSuperValue = function(){
return this.property;
};

function SubType(){
this.subproperty = false;
}

SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
return this.subproperty;
};

var instance = new SubType();
alert(instance.getSuperValue());//true
alert(instance instanceof Object);//true
alert(instance instanceof SuperType);//true
alert(instance instanceof SubType);//true

alert(Object.prototype.isPrototypeOf(instance));//true
alert(SuperType.prototype.isPrototypeOf(instance));//true
alert(SubType.prototype.isPrototypeOf(instance));//true

子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。给原型添加方法的代码一定要放在替换原型的语句之后。还有一点需要注意的是,通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。

原型链的问题:1、包含引用类型值的原型会被所有实例共享;2、不能向超类型的构造函数中传递参数。实践中很少会单独使用原型链。


5、组合继承

组合继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。

function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function(){
alert(this.name);
};

function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}

//继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
alert(this.age);
};

var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"

instance1.sayName();//"Nicholas";
instance1.sayAge();//29

var instance2 = new SubType("Greg",27);
alert(instance2.colors);//"red,blue,green"
instance2.sayName();//"Greg";
instance2.sayAge();//27

组合继承成为javascript中最常用的继承模式,而且,instanceof()、isPrototypeOf()也能够用于识别给予组合继承创建的对象。

6、原型式继承
道格拉斯克罗克福德2006年写了一篇文章,介绍了一种实现继承的方法,这种方法没有使用严格意义上的构造函数。他得想法是借助原型可以基于已有得对象创建新对象,同时还不必因此创建自定义类型。为了达到这个目的,他给出了如下函数:

function object(o){
function F(){}
F.prototype = o;
return new F();
}

在object()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个零食类型的一个新实例。从本质上讲,object()对传入其中的对象执行了一次浅复制。看下面的例子:

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");

alert(person.friends);//"Shelby,Court,Van,Rob,Barbie"


7、寄生式继承

寄生式继承时与原型式继承紧密相关的一种思路,同样由克罗克福德推出的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。

function createAnother(original){
var clone = object(original);//通过调用函数创建一个新对象
clone.sayHi = function(){//以某种方式增强这个对象
alert("hi");
};
return clone;//返回这个对象
}


8、寄生组合式继承

前面说过,组合继承是javascript最常用的继承模式,不过,它也有自己的不足。组合继承最大的问题就是无论什么情况下,都会调用两次超类型构造函数:一次是再创建子类型原型的时候,另一次是在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子类型构造函数时重写这些属性。再来看一看下面组合继承的例子:

function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function(){
alert(this.name);
};

function SubType(name,age){
SuperType.call(this.name);//第二次调用SuperType()
this.age = age;
}

SubType.prototype = new SuperType();//第一次调用SuperType()

SubType.prototype.sayAge = function(){
alert(this.age);
};

第一次调用SuperType构造函数时,SubType.prototype会得到两个属性:name和colors,它们都是SuperType的实例属性,只不过现在位于SubType的原型中。当调用SubType构造函数时,又会调用一次SuperType构造函数,这一次又在新对象上创建了实例属性name和colors。于是,这两个属性就屏蔽了原型中的两个同名属性。

图中,有两组name和colors属性:一组在实例上,一组在SubType原型中。这就是调用两次SuperType构造函数的结果。好在我们已经找到了解决这个问题方法——寄生组合式继承。所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成行驶来继承方法。其背后的基本思想是:不必为了制定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。寄生组合式继承的基本模式如下所示:

function inheritPrototype(subType,superType){
var prototype = object(superType.prototype);//创建对象
prototype.constructor = subType;//增强对象
subType.prototype = prototype;//指定对象
}

这个示例中的inheritPrototype()接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加constructor属性,从而弥补因重写原型而失去的默认的constructor属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以调用inheritPrototype()函数的语句,去替换前面例子中为子类型原型赋值的语句了。

function SuperType(name){
this.name = name;
this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = functon(){
alert(this.name);
};

function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}

inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge = function(){
alert(this.age);
};

这个例子的高效率体现在它只调用了一次SuperType构造函数,并且因此避免了在SubType.prototype上面创建不必要的,多余的属性。于此同时,原型链还能保持不变,因此,还能够正常使用instanceof和isPrototypeOf()。开发人员普遍认为寄生组合式继承时引用类型最理想的继承范式。在雅虎的YUI框架的YAHOO.lang.extend()中,实现了寄生调用继承。

下面是寄生组合式继承的一个完整的例子:

function object(o) {
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(subC, superC) {
var prototype = object(superC.prototype); //创建对象
prototype.constructor = subC; //增强对象
subC.prototype = prototype; //指定对象
}
var BaseClass = function() {
this.className = "Base";
};
BaseClass.prototype = {
showName: function() {
alert(this.className);
}
};
var SubClass = function() {
BaseClass.call(this);
this.classDesc = "SubClass";
};
inheritPrototype(SubClass, BaseClass);
SubClass.prototype.showDesc = function() {
alert(this.classDesc);
};


9、拷贝组合式继承

这个继承模式是来自博客园的村长赵大宝的一篇文章——《寻求完美之javascript继承》。改造的实质就是抛弃使用父类原型副本重写子类原型的做法。这种方式不仅兼顾了效率、继承实现、多态,而且不创建父类原型副本,即不发生原型的重写,避免了寄生组合继承存在的缺陷。另外,拷贝组合继承显而易见的对多重继承的实现提供了更优的支持。

function extend(subC, baseC) {
for (var ptototypeName in baseC.prototype) {
if (typeof(subC.prototype[ptototypeName]) === 'undefined') {
subC.prototype[ptototypeName] = baseC.prototype[ptototypeName]; //原型属性的拷贝
}
}
subC.prototype.constructor = subC; //增强
}
var BaseClass = function() {
this.className = "Base";
};
BaseClass.prototype = {
showName: function() {
alert(this.className);
}
};
var SubClass = function() {
BaseClass.call(this); //只执行一次父类构造函数
this.classDesc = "SubClass";
};
SubClass.prototype = {
showDesc: function() {
alert(this.classDesc);
}
};
extend(SubClass, BaseClass); //不破坏子类原型链的位置二