JavaScript之继承(原型链)

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

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

第一部分:原型链继承

  A

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

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

  


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中是否存在该方法,结果:存在。于是停止搜索。也就是说:在找不到属性或方法的情况下,搜索过程总是要一环一环地前行到原型链末端才会停下来。

   注意:instance.constructor现在指向的是SuperType,这是因为SubType的原型指向了另一个对象--SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。我们可以用以下代码做出验证:

1 console.log(instance.constructor);

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

  重要:别忘记默认的原型。我们知道,所有的引用类型都继承了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与构造函数之间的关系,如下所示:


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!

时间: 2024-08-29 03:07:42

JavaScript之继承(原型链)的相关文章

Javascript 组合继承 原型链继承 寄生继承

Javascript继承通常有三种方式. 第一种:组合式继承: function SuperType(name) { this.name = name; this.colors = ["red", "blue", "green"]; } SuperType.prototype.sayName = function() { console.log(this.name); }; function SubType(name, age) { //通过ca

JavaScript核心-继承-原型链

继承是面向对象的编程的一大特性,很多OO语言都支持两种继承方式:接口继承和实现继承.在ECMAScript中,由于函数没有签名,所以无法实现接口继承,只有实现继承. 实现继承主要是依靠原型链来实现的. 简单回顾一下构造函数.原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而对象的每个实例都有一个指向原型对象的内部指针. 再回顾一下原型对象的用途:用途是包含可以由特定类型的所有实例共享的属性和方法. 原型对象也是一个简单的对象,如果我们让一个原型对象等于另一个

对Javascript 类、原型链、继承的理解

一.序言 ??和其他面向对象的语言(如Java)不同,Javascript语言对类的实现和继承的实现没有标准的定义,而是将这些交给了程序员,让程序员更加灵活地(当然刚开始也更加头疼)去定义类,实现继承.(以下不讨论ES6中利用class.extends关键字来实现类和继承:实质上,ES6中的class.extends关键字是利用语法糖实现的) Javascript灵活到甚至可以实现接口的封装(类似Java中的Interface和implements). 二.类的实现 1.我对类的理解 ??首先,

图解JavaScript中的原型链

转自:http://www.jianshu.com/p/a81692ad5b5d typeof obj 和 obj instanceof Type 在JavaScript中,我们经常用typeof obj和obj instanceof Type来识别类型,那么两者的区别在哪?先来看两段代码 <!--typeof obj的方式判断--> <script>    var str = "toby";    console.log(typeof str);// stri

JavaScript继承-原型链继承

//原型链继承 function SuperType(){ this.name = 'super'; this.girlFriends = ["xiaoli","xiaowang"]; } SuperType.prototype.sayName = function(){ console.log(this.name); } function SubType(){ this.age = 20; } //创建SuperType的实例赋给SubType的原型 //实现继承

JS面向对象之继承——原型链

原型对象 每个javascript对象都有一个原型对象,这个对象在不同的解释器下的实现不同.比如在firefox下,每个对象都有一个隐藏的__proto__属性,这个属性就是“原型对象”的引用. 原型链 由于原型对象本身也是对象,根据上边的定义,它也有自己的原型,而它自己的原型对象又可以有自己的原型,这样就组成了一条链,这个就是原型链,JavaScritp引擎在访问对象的属性时,如果在对象本身中没有找到,则会去原型链中查找,如果找到,直接返回值,如果整个链都遍历且没有找到属性,则返回undefi

javaScript里的原型链

原型对象也是普通的对象,是对象一个自带隐式的__proto__属性,原型也有可能有自己的原型,如果一个原型对象的原型不为null的话,我们就称之为原型链.原型链是由一些用来继承和共享属性的对象组成的(有限的)对象链. 原型对象中的属性可以被多个实例共享.原型对象存在于构造函数的属性(prototype)中,prototype的值是一个Object类型数据(对象). JavaScript的数据对象有哪些属性值呢,举几个简单的栗子: writable:这个属性的值是否可以改: configurabl

夺命雷公狗---javascript NO:28 原型链

1.原型对象 在JavaScript中,每个构造器在加载后都会自动生成一个对象,我们把这个对象就称之为原型对象. 2.构造器与原型对象的关系 Person构造器与Person原型对象在内存中表现为相互独立,互不影响.但是在Person构造器中存在一个prototype属性指向Person原型对象,同时在Person原型对象中也存在一个属性指向Person构造器. 3.原型对象的作用 当我们在Person构造器的实例对象中引用一个不存在的属性或方法,系统会自动到Person构造器的原型对象中去寻找

Javascript中的原型链

说到原型链,首先得说一下对象. 在javascript中我们经常说"一切皆为对象",而对象又是属性的集合,但并不是所有的类型都是对象,undefined, number, string, boolean都属于简单的值类型,不是对象.我们所说的可以作为对象的有:数组.函数.对象.Null.New Number(). 所以函数是一种对象,但是一切对象又是函数创建的,有人可能会说不对啊,例如下面这种的,obj是对象,但是并没有函数啊. var obj = { a: 10, b: 20 };

JavaScript进阶之原型链

对象 1 function f1(){ 2 }; 3 typeof f1 //"function"函数对象 4 5 6 var o1 = new f1(); 7 typeof o1 //"object"普通对象 8 9 var o2 = {}; 10 typeof o2 //"object"普通对象 JavaScript中将对象分为普通对象和函数对象. 使用函数对象可以创建普通对象,普通对象没法创建函数对象. 凡是通过new Function创建