javascript中继承(一)-----原型链继承的个人理解

【寒暄】好久没有更新博客了,说来话长,因为我下定决心要从一个后台程序员转为Front End,其间走过了一段漫长而艰辛的时光,今天跟大家分享下自己对javascript中原型链继承的理解。

总的说来,js中的常用的继承方式可以分为两种,一种是原型链式继承,这也是本文要谈的重点;另外一种是借用构造函数继承,这方面的理解,我将在下次的博客中更新。好了,闲话不多说,进入正题。

一,关于原型

首先,我们不得不解释下原型的概念:我们创建的每一个函数都有一个原型属性,即prototype,这个属性是一个指针,指向原型对象。
 function Person(){}//这里我们声明一个函数Person,js中函数是对象,也是构造函数
console.log(Person.prototype)//打印一下Person对象的原型,会出现什么呢?如下图所示:

javascript中继承(一)-----原型链继承的个人理解

大家在图中看到了,Person对象的原型拥有一个constructor,它指向Person的构造函数,即Person本身,另外一个属性是__proto__属性,这个属性我会在后文中说明。

到这里,大家肯定会明白了,一个对象建立后,会产生一个局部的“小链式结构”,即Person对象拥有一个prototype属性,这个属性指向原型对象,在原型对象中又有一个构造器constructor,指向构造函数。用一张图来说明:

javascript中继承(一)-----原型链继承的个人理解

那么,原型对象的作用是什么呢?这个原型对象包含由特定类型的实例共享的属性和方法。大家要注意共享这两个字,用一段代码解释下

 function Person(){
this.name="bob" //这是一个实例属性
}
Person.prototype.eat=function(){ //给对象的原型对象添加一个eat的方法,接下来,new的实例会共享这个方法
return "food";
}
var p1=new Person(); //这里究竟发生生了什么?
p1.eat()//->food
var p2=new Person();
p2.eat()//->food,所以只要是Person的对象,他们都会共享原型对象的方法,当然,p1.name也会共享Person的实例属性,因为p1是Person的一个实例

好了,到这里原型的概念我们已经讲完了,大家或许会疑问,上面的new一个Person实例的过程中究竟发生了什么呢?为什么这个实例能够访问到原型对象中的方法?其实,在这个过程过程中,p1实例拥有了一个指针,这个指针指向构造函数的原型对象。此时原型对象中的方法自然能够被实例所访问。用一张图来说明下:

javascript中继承(一)-----原型链继承的个人理解

这里,我们总结下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象拥有一个指向构造函数的指针,而实例拥有一个指向原型对象的内部指针(这就是前面所提到的[[Prototype]],即__proto__,要注意的是这个__proto__属性在chrome浏览器中是可以看到的,而在大部分浏览器是隐藏的!)

二,关于原型链继承

好了,说了这么多终于到回到我们的主角了【原型链】,提出一个思考:如果我们让原型对象等于另外一个对象的实例,将会有一个什么样的结果呢?先看下面一段代码

 function Person(){
this.name="bob";
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
Student.prototype=new Person();//将Person实例赋给Student的原型对象
var one=new Student();
one.name//bob
one.eat()//food,Student的实例能访问到Person对象的实例方法,也能访问到其原型属性中的方法

以上就是原型链继承的一种基本模式,那么我们怎么解释这样的原理呢?之前说过,对象的实例拥有一个指向原型对象的指针,那么student的原型对象拥有了Person对象实例后,自然也拥有一个指向Person原型对象的指针。此时,我们再new一个Student实例one时,one实例包含一个指向Student原型的指针,而Student.prototype拥有一个指向Person原型对象的指针,Person原型本身包含一个指向自身构造函数的指针。这样一来,就构成了实例与原型的链条。这就是所谓的原型链的概念!

用一张图描绘一下上面讲的情况:

javascript中继承(一)-----原型链继承的个人理解

大家要注意一下,这里的one对象的constructor现在指向谁呢?它并不指向Student,因为Student的原型指向另一个对象--Person的原型,而这个原型对象的constructor指向的是Person。

三,原型链方法的改写及注意的问题

有时候,子类型需要改写超类型当中的方法,或者添加新的方法,一定要注意给原型添加代码一定要放在继承语句(即替换原型语句)的后面,
function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){}
//Student.prototype.eat=function(){
// return "food1";
//}
//注意如果更改原型语句的代码放在替换之前,那么下面one.eat()的结果将仍然是food
//,原因很简单,前面对prototype对象的修改,在后面的替换一句中被Person实例对象覆盖了
//,换句话说,就是现在的prototype实例中仍旧是以前的eat方法
Student.prototype=new Person();
Student.prototype.eat=function(){
return "food1";
}
var one=new Student();
console.log(one.eat());//food1

但是大家要注意一下一种情况,在通过原型链继承时,不能通过对象字面量个方式来更新原型对象

 function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){} Student.prototype=new Person();
Student.prototype={
run:function(){
return "run";
}
};
var one=new Student();
console.log(one.eat());//Uncaught TypeError: undefined is not a function
在上面的代码中,把Person的实例赋给Student的原型,接下来又把原型改写成另一个对象字面量,现在原型包含的是Object实例,不再是Person实例,因此原型链已经被切断了,也就是说Student和Person没关系了。
但是思考下面一段代码,我这样改写,会切断原型链吗?
 function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){} Student.prototype=new Person();
Student.prototype.constructor=Student;//把Student原型对象中原本指向Person构造函数的对象强行指向到Student
var one=new Student();
console.log(one.eat());//food

从代码运行的情况来看,这个动作并没有切断原型链的继承,原因何在?

大家一定要主要实例和原型对象是通过[[Prototype]]来实现关系链接的,换句话说,实例里面的[[Prototype]]指针指向原型对象,这里我改写了constructor后,并没有将[[Prototype]]的指向改变,当然也就没有改变整个原型链的继承关系!这一点要非常注意!!!

四,如何确定原型和实例关系

可以通过两种方式来确定原型和实例之间的关系,第一种是instanceof操作符,这个操作符用来测试实例与原型链中出现过的构造函数,看下面的一段代码
 function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){} Student.prototype=new Person();
Student.prototype.constructor=Student;
var one=new Student();
var person=new Person();
console.log(one instanceof Student);//true
console.log(one instanceof Person);//true
console.log(person instanceof Person);//true
console.log(person instanceof Student);//false

最后一个出现了false,什么原因,instanceof的工作原理是什么呢?

A intanceof B,它的工作原理是,检测对象B的prototype指向的对象是否出现在Ad对象的[[Prototype]]链上,换句话说,A对象的原型链继承线路上,有没有B的存在。最后一个例子中Student的原型对象指向Person的原型对象,person实例也是指向Person的原型对象,而Person的原型对象指向了Person的构造函数本身,所以这条person对象的原型链路上,并没有出现Student的原型对象,故最后一个person不是Student的实例!
 
那么,第二种检测方式是isPrototypeOf()方法,同理,只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型。
function Person(){
this.name="bob" ;
}
Person.prototype.eat=function(){
return "food";
}
function Student(){} Student.prototype=new Person(); var one=new Student();
var person=new Person();
console.log(Student.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(one));//true
console.log(Person.prototype.isPrototypeOf(person));//true
console.log(Student.prototype.isPrototypeOf(person));//false

五,总结

以上就是原型连接继承及需要注意的问题,原型链继承固然很强大,但是也有一些问题,比如共享的原型属性容易被修改,在创建子类型的实例时,不能向超类传参数,等等,在下一次的分享中,我将谈一下自己学习‘借用构造函数实现继承’的心得!
【感谢】:本人是刚学习前端的菜鸟,以上都是自己的有些学习体会,讲的啰嗦和不对的地方,大家可以给我留言,要知道这篇博文我整整花了一个晚上的时间完成,希望能给那些正在学习js的同学一些参考。感谢《js高级程序设计》,以及《javascript类型检测》这篇文章!
PS:本文中的图片水印,是本人csdn的博客搬家到cnblogs所致,版权均归属本人!
上一篇:Ubuntu平台rm误删的文件如何恢复


下一篇:WebService从服务端到客户端的用例