JavaScript之继承(原型链)

  咱们知道继承是oo语言中不可缺乏的一部分,对于JavaScript也是如此。通常的继承有两种方式:其一,接口继承,只继承方法的签名;其二,实现继承,继承实际的方法。JavaScript不支持签名,所以只有实现继承。其中实现继承主要是依赖于原型链的。下面我将以原型链为重点说说继承的几种主要的方式:html

  • 原型链继承
  • 借用构造函数继承
  • 组合继承(重点)

第一部分:原型链继承

  A数组

  要说原型链继承,不得不首先介绍一下原型链的概念。app

  想象一下,若是使原型对象等于另外一个对象的实例,则此时原型对象将包含一个指向另外一个原型的指针。相应地,另外一个原型也将包含指向另外一个构造函数的指针。假设另外一个原型又是另外一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条(注意:这里的实例和原型都是相对的),这即是原型链的基本概念。函数

  

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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();
console.log(instance.getSuperValue()); //true

  在上述代码中,咱们能够看出subType的原型是SuperType的实例,所以,原来存在于SuperType的实例中的全部属性和方法,如今也存在于SubType.prototype中了。且咱们没有使用SubType默认提供的原型对象,而是给它换了一个新原型对象(即SuperType的实例)。所以,新原型对象不只具备做为一个SuperType的实例所拥有的所有属性和方法,并且其内部还有一个指针,指向了SuperType的原型。即:instance指向SubType的原型,SubType的原型指向了SuperType的原型。值得注意的是:property如今位于SubType.protoType中(由于SuperType构造函数中的this指向的是建立的对象实例)。性能

  当以读取模式访问一个实例属性时,搜索过程会沿着原型链向上进行搜索。好比,调用instance.getSuperValue()会经历三个搜索步骤:(1).搜索实例中是否存在该方法,结果:无。(2).沿着原型链向上,搜索SubType.prototype中是否存在该方法,结果:无。(3).继续沿着原型链,搜索SuperType.prototype中是否存在该方法,结果:存在。因而中止搜索。也就是说:在找不到属性或方法的状况下,搜索过程老是要一环一环地前行到原型链末端才会停下来。this

   注意:instance.constructor如今指向的是SuperType,这是由于SubType的原型指向了另外一个对象--SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。咱们能够用如下代码作出验证:spa

1 console.log(instance.constructor);

  最终返回的是SuperType这个构造函数。prototype

  重要:别忘记默认的原型。咱们知道,全部的引用类型都继承了Object,而这个继承也是经过原型链实现的,即全部函数的默认原型都是Object的实例,所以默认原型都会包含一个内部指针,指向Object.prototype。这也是全部引用类型都会继承toString()、valueOf()方法的根本缘由。咱们可使用下面代码作出验证:指针

1
2
3
console.log(Object.prototype.isPrototypeOf(instance)); //true
console.log(SuperType.prototype.isPrototypeOf(instance)); //true
console.log(SubType.prototype.isPrototypeOf(instance)); //true

  也就是说instace实例对象的原型对象分别是Object.prototype、SuperType.prototype、SubType.prototype。另外咱们还可使用instanceof操做符判断,实例instance与构造函数之间的关系,以下所示:code

1
2
3
console.log(instance  instanceof  Object); //true
console.log(instance  instanceof  SuperType); //true
console.log(instance  instanceof  SubType); //true

  即instance是Object SuperType SubType的实例。下面咱们使用一张图表表示他们之间的关系。

  这里,咱们能够认为加粗的线条就是原型链(实例与原型的链条)。

  从这张图表中,咱们能够看到SubType Prototype是没有constructer属性的,更没有指向SubType构造函数,这是由于建立SubType构造函数同时建立的原型对象和这个原型对象不是同一个,这个原型对象是SuperType的实例。注意到,后两个原型对象都有一个[[prototype]]属性,由于这时他们是被看成实例来处理的。

 

  B

  谨慎地定义方法

  当子类型有时候须要覆盖(与原型中覆盖属性是一样的道理,见《深刻理解JavaScript中建立对象模式的演变(原型)》)超类型的某个方法,或者须要添加超类型中不存在的某个方法。这时,应当注意:给原型添加方法的代码必定要放在(用超类型的对象实例做为子类型的原型来)替换原型的语句以后。看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
     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; //这时在子类型中新添加的方法
}
SubType.prototype.getSuperValue= function (){
     return  false ; //这时在子类型添加的超类型的同名方法,用于覆盖超类型中的方法,所以,最后反悔了false
}
var  instance=  new  SubType();
console.log(instance.getSuperValue()); //false

  

  若是顺序颠倒,那么这两个新添加的方法就是无效的了,最终instance.getSuperValue()获得的结果仍然是从超类型中搜索到的,返回false这时由于若是颠倒,那么后面添加的方法给了SubType最开始的原型,后面替换原型以后,就只能继承超类型的,而刚刚添加的方法不会被实例所共享,此时实例的[[prototype]]指向的是替换以后的原型对象而不在指向最初的添加了方法的原型对象。

  还有一点须要注意的就是,在经过原型链实现继承时,不能使用对象字面量建立原型方法(这样就会再次建立一个原型对象,而不会刚刚的那个用超类型的实例替换的对象),由于这样会切断原型链,没法实现继承。

  C

  单独使用原型链的问题

 问题1: 最主要的问题是当包含引用类型值的原型。首先,回顾如下原型模式建立对象的方法,对于包含引用类型值的原型属性会被全部的实例共享,这样改变其中一个实例,其余都会被改变,这不是咱们想要的。这也正是以前关于原型的讲解中为何要将引用类型的值定义在构造函数中而不是定义在原型对象中。对于原型链,也是一样的问题。

  看如下的代码;

   

1
2
3
4
5
6
7
8
9
10
     function  SuperType(){
    this .colors=[ "red" , "blue" , "green" ];
}
function  SubType(){}
SubType.prototype= new  SuperType(); //这时,SuperType中的this对象指向的是SubType.prototype
var  instance1= new  SubType();
instance1.colors.push( "black" );
console.log(instance1.colors); //["red", "blue", "green", "black"]
var  instance2= new  SubType();
console.log(instance2.colors); //["red", "blue", "green", "black"]

  

  在SuperType构造函数中的this必定是指向由他建立的新对象的,而SubType.prototype正是这个新对象,所以SubType的原型对象便有了colors属性,因为这个属性值是数组(引用类型),于是尽管咱们的本意是向instance1中添加一个“black”,但最终不可避免的影响到了instance2。而colors放在构造函数中有问题,若是放在其余的原型对象中,依然会有问题。所以,这是原型链继承的一个问题。

  

  问题二:

  在建立子类型的实例时,不能向超类型的构造函数传递参数。实际上,应该说没有办法在不影响全部对象实例的状况下,给超类型的构造函数传递参数。

  正由于单单使用原型链来实现继承出现的以上两个问题,咱们在实践中不多会单独使用原型链。

 

 

第二部分:借用构造函数继承

  A

  为解决以上问题,人们发明了借用构造函数(又称伪造对象或经典继承),这种方法的核心思想是:在子类型构造函数的内部调用超类型构造函数。因为函数只不过是在特定环境中执行代码的对象,所以经过使用apply()和call()方法也能够在(未来)新建立的对象上执行构造函数。注意:这种继承方式没有用到原型链的知识,与基于原型链的继承毫无关系。代码以下:

1
2
3
4
5
6
7
8
9
10
11
function  SuperType(){
     this .colors=[ "red" , "blue" , "green" ];
}
function  SubType(){
     SuperType.call( this ); //在子类型构造函数的内部调用超类型构造函数
}
var  instance1= new  SubType();
instance1.colors.push( "black" );
console.log(instance1.colors); //["red", "blue", "green", "black"]
var  instance2= new  SubType();
console.log(instance2.colors); //["red", "blue", "green"]

  首先,咱们能够看到此种继承方式既完成了继承任务,又达到了咱们但愿达到的效果:对一个实例的值为引用类型的属性的修改不影响另外一个实例的引用类型的属性值。

  值得注意的是:这种继承方式与原型链的继承方式是彻底不一样的。看如下代码:

1
2
console.log(instance1  instanceof  SubType); //true
console.log(instance1  instanceof  SuperType); //false

  instance1和instance2都不是SuperType的实例。这里的继承只是表面上的继承。咱们能够分析一下这个继承的过程:首先声明了两个构造函数,而后执行var instance1=new SubType();即经过new调用了构造函数SubType,既然调用了SubType构造函数,此时便进入了SubType执行环境,该环境中又调用了SuperType()函数(注意:这里未使用new,故此时应当把SuperType函数看成通常函数来处理),又由于SubType()中this是指向instance1(SubType是构造函数啊!)的,因此,接下来就会在instance1对象上调用普通函数SuperType,由于这个普通函数在instance1上被调用,所以,SuperType中的this又指向了Instance1,这是,instance1对象便添加了属性值为应用类型的colors属性,instance2同理。

  这解决了原型链继承中的第一个问题。

  

  B

  相对于原型链而言,借用构造函数有一个很大的优点,便可以在子类型构造函数中向超类型构造函数传递参数。以下所示:

1
2
3
4
5
6
7
8
9
10
function  SuperType(name){
     this .name=name;
}
function  SubType(){
     SuperType.call( this , "zzw" );
     this .age=21;
}
var  instance1= new  SubType();
console.log(instance1.name); //zzw
console.log(instance1.age); //21

  其中SuperType.call(this,"zzw");又能够写作SuperType.apply(this,["zzw"]);(关于这一部分知识点能够看《JavaScript函数之美~》第三部分)。

  言归正传,让咱们先分析函数时如何执行的:首先声明了两个构造函数,而后经过new操做符调用了Subtype构造函数,随即进入Subtype构造函数的执行环境,执行语句SuperType.call(this.zzw);,随即进入了普通函数(一样地,只要没有使用new操做符,它就是通常函数)的执行环境并传递了参数,且使用了call方法,说明在instance1对象上调用普通函数SuperType,由于在对象上调用的,因此SuperType函数中的this指向instance1,并最终得到了name属性。SuperType函数执行环境中的代码执行完毕以后,执行环境又回到了SubType构造函数,这时,instance对象又得到了属性值为21的age属性。

  ok!借用构造函数继承又解决了原型链继承的第二个问题。

  然而,借用构造函数就没有缺点吗?答案是有!由于仅仅使用借用构造函数,就没法避免构造函数模式的问题--方法在构造函数中定义(而致使浪费)。并且,咱们说这种方式与原型链不一样,所以在超类型的原型中定义的方法,对子类型而言也是不可见的,结果全部类型都只能使用构造函数模式。

  考虑到上述问题,借用构造函数的技术也是不多单独使用的。

 

 

第三部分:组合继承(伪经典继承)

  与建立对象时,咱们将自定义构造函数模式和原型模式组合同样,这种继承方式即将原型链和借用构造函数的技术组合到一块儿,从而发挥二者之长。主要思想是:使用原型链实现对原型属性(即但愿让各个实例共享的属性)和方法(对于借用构造函数,继承方法显然是不合适的)的继承,而经过借用构造函数来实现对实例属性(即不但愿共享的属性,以前方法是经过实例属性覆盖原型属性)的继承。这样,既经过在原型上定义方法实现了函数复用(即只建立一次方法,被屡次使用,若是将函数定义在构造函数中,建立一个实例,就会同时建立一个相同的方法,没法复用,影响性能),又可以保证每一个实例都有本身的属性(由于借用构造函数能够传递参数啊!把实例属性经过借用构造函数实现,就不用去覆盖了)。

  

下面来看这样一个例子:

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(name,age){
     this .name=name; //实例属性使用借用构造函数模式               this.age=age;//实例属性使用借用构造函数模式
     this .colors=[ "red" , "blue" , "green" ]; //这个数组虽然会同时被原型链和借用构造函数添加使用,但最后根据原型链的搜索机制,是按照借用构造函数模式实现的。
}
SuperType.prototype.sayName= function (){
     console.log( this .name); //实现一样效果的方法使用原型链模式
};
function  SubType(name,age){
     SuperType.call( this ,name,age); //借用构造函数模式的有点就是能够向子类型构造函数中的超类型构造函数传递参数,这里this的用法很重要
     
};
SubType.prototype= new  SuperType(); //使用SuperType的实例来替换为SubType的原型对象
SubType.prototype.constructor=SubType; // 这句代码即将SubType的原型对象的constructor属性指向SubType,但这一句去掉也不会影响结果。
SubType.prototype.sayAge= function (){
     console.log( this .age); //在原型对象中定义方法,可使得该方法实现复用,加强性能
};
var  instance1= new  SubType( "zzw" ,21);
instance1.colors.push( "black" );
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //zzw
instance1.sayAge(); //21
var  instance2= new  SubType( "ht" ,18);
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //ht
instance2.sayAge(); //18

 

  关键点:在SuperType构造函数中代码this.colors=["red","blue","green"];实际上也会向单独的原型链继承那样,将colors数组添加到SubType的原型对象中去,可是借用构造函数在执行时会将colors数组直接添加给实例,因此,访问colors数组时,根据原型链的搜索机制,在实例中的colors数组一旦被搜索到,就不会继续沿着原型链向上搜索了(屏蔽做用)。所以最终instance1的colors的改变并不会影响到instance2的colors数组的改变(二者的colors数组都来自实例自己而不是原型对象)。

 

 

 

  只会幻想而不行动的人,永远也体会不到收获果实时的喜悦。 Just do it!

相关文章
相关标签/搜索