这是关于原型链的一系列的现象与原理的解释以及例子
分成4个部分阐述: 1.如何创建一个对象
2.使用原型链prototype实现对象的继承.
3.原型链上属性和方法的联系与规则
4.深入剖析原型链。
一、如何创建一个对象:
1.使用关键字new创建。
var obj=new Object;
或者
function c()
{}
var obj=new c();
2.使用花括号创建。
var obj={};
我们知道,function是定义一个函数,里面的var 都是局部变量,那么如何声明一个对象的属性和方法呢。秘密就在于this关键字。在函数内部使用this关键字,就相当于是对象的属性和方法的声明这个变量是存在的。具体的例子如下:
function a()
{
this.a=1;
this.get=function()
{
return this.a;
}
}
那么只要使用用var obj =new a();即创建一个对象的实例,里面就包括function里面的this属性和方法的就会直接拷贝到对象变量obj上。直接使用a.a的方法即可调用相应的属性和方法。
但是注意,除非实例化function a(),否则无法调用里面的this关键字指向的属性以及方法。
因为此时this上定义的属性以及方法类似于C++/JAVA等面向对象声明的属性以及方法。因此,这种定义函数的方法也被称为JS的构造函数。
二、使用原型链prototype实现对象的继承.
所有函数function都有一个默认的属性prototype,通过用typeof函数测试我们可以看到prototype是一个对象。
alert(typeof(a.prototype));
结果:
那么这个prototype的属性到底有什么用呢~?
既然是一个对象那么赋值的时候就当然是付给prototype一个实例了。事实上,只要把一个实例赋给prototype这个属性,那么这个function就具有这个实例的属性和方法。相当于我们常说的继承。请看下面的代码:
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
var obj =new b();//实例化函数b
alert(obj.get());
就是这么简单的逻辑,我们就是使用对象b实例化的obj,输出继承了父类a的方法get。输出父类a里面的属性ia。大家可以看到,这样就轻松地实现了继承。
那如果a又继承另外一个对象x怎么办?
答案当然是b会继承a所继承的所有东西。而他们的关系就像用prototype属性连起来一样。所以我们可以看成也是一个由prototype连接而成的链表,人称原型链。
三、原型链上属性和方法的联系与规则
首先我们明确一个概念,原型链就是一串好像用属性prototype连接而成的,每个节点具有属性和方法甚至函数过程的链。这种链式结构我们无疑可以发现一个很重要的问题----链上的节点之间发生属性或者方法重名怎么办?下面我们从读和写两方面来解析这条原型链的属性和方法的使用过程。
读:
从上面的例子里,alert(obj.get())输出的结果是什么呢,如果运行过一次,我们可以发现输出的结果是1.
但是我们可以看到,构造函数b定义的时候是没有get的方法的,同时,get方法调用的属性ia也是b中不存在的,那么我们在拥有prototype继承的构造函数所实例化的对象是如何调用属性和方法的呢。
结论是:对于调用实例化某一构造函数的对象的方法或者属性的时候,js会在原型链最结尾的点,也就是C++/JAVA上所描述的子类,上述例子中的b函数上,查找是否有所需要的方法或者属性,如果找不到的话,就会根据赋值给prototype的父节点对象,父构造函数上查找对应的属性和方法,就这样按照这种规则查找出对应名称的属性或方法。如果链上节点都不存在的话,当然就会输出undefined。
如果子节点有该属性或者方法,同时父节点也有的时候,当然会优先考虑子节点的,这也类似于C++/JAVA上所描述的覆盖的概念,子类的属性或者方法覆盖父类的。
而在上述例子中,get方法调用了一个this.ia,那么查找此此元素的时候,也会遵循上诉的原则,先查找构造函数b上是否有属性ia,没有的话再查找a上的构造函数上的属性ia。最后输出结果1。如果我们在构造函数b上添加一个属性this.ia=2,那么结果会如上述结论所说输出2.
写:
我们都知道,new操作会建立一个和构造函数一模一样的副本,开辟一个空间存放对象。但是我们从上面的操作可以看到,对于函数链上的属性,都可以被各个实例所访问,那么,js在实例化有prototype继承的构造函数的时候,是把链上的节点都实例化一次,还是被所有实例所共享呢?下面我们看下面例子。
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
var obj1 =new b();//实例化函数b
var obj2 =new b();//实例化函数b
alert(obj1.ia);//输出1,因为他是父类a中的ia
alert(obj2.ia);//输出1,因为他是父类a中的ia
obj1.ia=2;
alert(obj1.ia);//输出2,因为写操作会在实例对象建立一个副本ia并且赋值2,无修改其原型链上的父节点。
alert(obj2.ia);//输出还是1,因为他是父类a中的ia
b.prototype.ia=3;//修改原型链上的属性ia。
alert(obj1.ia);//输出2,因为已经在实例上建立了副本,所以优先读取副本。
alert(obj2.ia);//输出3,因为子类b中不存在,所以还是查找父节点(prototype对象上的对象)上的ia,由于父节点上的ia已经被修改成3,所以输出3.
上面的例子说明了2件事:
1.写操作并不是直接查找原型链上的属性,然后更改他,而是直接在实例上创建在一个同名对象。由于实例上已经存在这个属性,所以下次读取的时候优先读取这个已存在的,相当于原型链上的原属性被覆盖(但是还是存在的)。
2.Prototype链上的节点(父类)都是被子类实例化后的对象所共享,也就是只有唯一的一个对象prototype,因此当我们修改b.prototype.ia=3;/后,obj2输出的也是3,说明他是被其他没有创建ia属性副本的实例所共享的。
这样我们就清晰了,原型链只有一条,并且不会因为子类实例化和把链上的节点,属性与方法都实例化,他将被所有子类的实例化对象所共享。
而这个原型链上的父类是准读不准写(通过子类实例化对象改变链上节点的属性与方法)。一旦你的实例化对象进行和父类属性与方法同名的写操作时,就会在实例化对象上建立对应的副本,同时进行写操作,此时这个实例也覆盖了原型链上的这个属性和方法。这种方法很好的保护了原型链上节点的共享性,不用担心不小心被其他实例所修改。这样大大地节省了内存,可以在频繁的对象创建中,有效地减少了内存空间的使用。
当然可以通过函数b上的prototype对象来修改链上的属性与方法,这样就可以做到一改全改,让子类共享的属性发生改变。如上面的 b.prototype.ia=3;操作。
这种做法也衍生出一种叫做延时绑定的思想,在使用的时侯才进行属性和方法的绑定,这种行为是可以通过原型链、prototype这个属性实现的。这样可以很方便地应用到你的编程中,但是注意,这种方法还是弊大于利的,因为可以在任何地方进行重绑定。所以程序者也许不知道在何时进行了绑定,使得代码很混乱,同时在大代码量的情况下,照成不可预料的结果。因此,延时绑定必须慎用。
四、深入剖析原型链。
上面说到,原型链是由prototype连在一起的,事实上这种说法是不严谨,甚至可以说是错误的,因为prototype是一个对象,并且实例是没有prototype这个属性的,但是,有比prototype更好的,用来表现原型链的东西,这是对象实例后的一个私有属性,ie下是不可访问的,但是用firefox是可以查看的,它才是真正的作为实例在原型链上查找属性与方法的一个类似指针的属性。可以这么说,prototype是函数所有的,而__proto__是实例化的对象存在的属性。我们常把这个__proto__指向的原型成为父原型。因为prototype是一个对象的实例,所以原型也有父原型。下面给出一段例子来解释这个现象。
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
alert(a instanceof Function);//a是Function的实例;
alert(a.__proto__ === Function.prototype);//a的父原型指向到Function的原型;
alert(b.__proto__ === Function.prototype);//a的父原型指向到Function的原型
var obj =new b();//实例化函数b
alert(typeof(obj.__proto__));//输出obejct,说明是一个对象
alert(obj instanceof Object);//ture.obj是其中一个对象
alert(obj.__proto__===b.prototype);//ture。因为__prototype指向父原型
alert(obj.__proto__.__proto__===a.prototype);//ture。因为__prototype指向父原型
alert(obj.__proto__.__proto__.__proto__===Object.prototype);//ture。因为__prototype指向父原型
alert(Object.prototype.__proto__);//null。因为Object的原型是所有父原型的顶端,它不再具有父原型;
alert(Function instanceof Object);//Function是Object的实例;
alert(Function.__proto__ === Function.prototype);//Function的父原型指向到Function的原型;
alert(Function.prototype.__proto__ === Object.prototype);//Function的原型的父原型指向到Object的原型
alert(Object.__proto__ === Function.prototype);//Object的父原型指向到Function的原型;
从上面的例子我们再也清晰不过了,实例查找原型链的时候使用的__proto__ 这个私有属性,通过这个属性一层一层地遍历原型链,而原型链的最终父原型是Object.prototype,它是所有实例的父原型,只要修改这个父原型,那么所有的实例都将用到修改的属性。
同时。Function.prototype是所有函数的父原型,而它Function.prototype的父原型也是Object.prototype。到了这里,我们可以清晰地看到一个实例后面的原型链的全貌了。
分成4个部分阐述: 1.如何创建一个对象
2.使用原型链prototype实现对象的继承.
3.原型链上属性和方法的联系与规则
4.深入剖析原型链。
一、如何创建一个对象:
1.使用关键字new创建。
var obj=new Object;
或者
function c()
{}
var obj=new c();
2.使用花括号创建。
var obj={};
我们知道,function是定义一个函数,里面的var 都是局部变量,那么如何声明一个对象的属性和方法呢。秘密就在于this关键字。在函数内部使用this关键字,就相当于是对象的属性和方法的声明这个变量是存在的。具体的例子如下:
function a()
{
this.a=1;
this.get=function()
{
return this.a;
}
}
那么只要使用用var obj =new a();即创建一个对象的实例,里面就包括function里面的this属性和方法的就会直接拷贝到对象变量obj上。直接使用a.a的方法即可调用相应的属性和方法。
但是注意,除非实例化function a(),否则无法调用里面的this关键字指向的属性以及方法。
因为此时this上定义的属性以及方法类似于C++/JAVA等面向对象声明的属性以及方法。因此,这种定义函数的方法也被称为JS的构造函数。
二、使用原型链prototype实现对象的继承.
所有函数function都有一个默认的属性prototype,通过用typeof函数测试我们可以看到prototype是一个对象。
alert(typeof(a.prototype));
结果:
那么这个prototype的属性到底有什么用呢~?
既然是一个对象那么赋值的时候就当然是付给prototype一个实例了。事实上,只要把一个实例赋给prototype这个属性,那么这个function就具有这个实例的属性和方法。相当于我们常说的继承。请看下面的代码:
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
var obj =new b();//实例化函数b
alert(obj.get());
就是这么简单的逻辑,我们就是使用对象b实例化的obj,输出继承了父类a的方法get。输出父类a里面的属性ia。大家可以看到,这样就轻松地实现了继承。
那如果a又继承另外一个对象x怎么办?
答案当然是b会继承a所继承的所有东西。而他们的关系就像用prototype属性连起来一样。所以我们可以看成也是一个由prototype连接而成的链表,人称原型链。
三、原型链上属性和方法的联系与规则
首先我们明确一个概念,原型链就是一串好像用属性prototype连接而成的,每个节点具有属性和方法甚至函数过程的链。这种链式结构我们无疑可以发现一个很重要的问题----链上的节点之间发生属性或者方法重名怎么办?下面我们从读和写两方面来解析这条原型链的属性和方法的使用过程。
读:
从上面的例子里,alert(obj.get())输出的结果是什么呢,如果运行过一次,我们可以发现输出的结果是1.
但是我们可以看到,构造函数b定义的时候是没有get的方法的,同时,get方法调用的属性ia也是b中不存在的,那么我们在拥有prototype继承的构造函数所实例化的对象是如何调用属性和方法的呢。
结论是:对于调用实例化某一构造函数的对象的方法或者属性的时候,js会在原型链最结尾的点,也就是C++/JAVA上所描述的子类,上述例子中的b函数上,查找是否有所需要的方法或者属性,如果找不到的话,就会根据赋值给prototype的父节点对象,父构造函数上查找对应的属性和方法,就这样按照这种规则查找出对应名称的属性或方法。如果链上节点都不存在的话,当然就会输出undefined。
如果子节点有该属性或者方法,同时父节点也有的时候,当然会优先考虑子节点的,这也类似于C++/JAVA上所描述的覆盖的概念,子类的属性或者方法覆盖父类的。
而在上述例子中,get方法调用了一个this.ia,那么查找此此元素的时候,也会遵循上诉的原则,先查找构造函数b上是否有属性ia,没有的话再查找a上的构造函数上的属性ia。最后输出结果1。如果我们在构造函数b上添加一个属性this.ia=2,那么结果会如上述结论所说输出2.
写:
我们都知道,new操作会建立一个和构造函数一模一样的副本,开辟一个空间存放对象。但是我们从上面的操作可以看到,对于函数链上的属性,都可以被各个实例所访问,那么,js在实例化有prototype继承的构造函数的时候,是把链上的节点都实例化一次,还是被所有实例所共享呢?下面我们看下面例子。
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
var obj1 =new b();//实例化函数b
var obj2 =new b();//实例化函数b
alert(obj1.ia);//输出1,因为他是父类a中的ia
alert(obj2.ia);//输出1,因为他是父类a中的ia
obj1.ia=2;
alert(obj1.ia);//输出2,因为写操作会在实例对象建立一个副本ia并且赋值2,无修改其原型链上的父节点。
alert(obj2.ia);//输出还是1,因为他是父类a中的ia
b.prototype.ia=3;//修改原型链上的属性ia。
alert(obj1.ia);//输出2,因为已经在实例上建立了副本,所以优先读取副本。
alert(obj2.ia);//输出3,因为子类b中不存在,所以还是查找父节点(prototype对象上的对象)上的ia,由于父节点上的ia已经被修改成3,所以输出3.
上面的例子说明了2件事:
1.写操作并不是直接查找原型链上的属性,然后更改他,而是直接在实例上创建在一个同名对象。由于实例上已经存在这个属性,所以下次读取的时候优先读取这个已存在的,相当于原型链上的原属性被覆盖(但是还是存在的)。
2.Prototype链上的节点(父类)都是被子类实例化后的对象所共享,也就是只有唯一的一个对象prototype,因此当我们修改b.prototype.ia=3;/后,obj2输出的也是3,说明他是被其他没有创建ia属性副本的实例所共享的。
这样我们就清晰了,原型链只有一条,并且不会因为子类实例化和把链上的节点,属性与方法都实例化,他将被所有子类的实例化对象所共享。
而这个原型链上的父类是准读不准写(通过子类实例化对象改变链上节点的属性与方法)。一旦你的实例化对象进行和父类属性与方法同名的写操作时,就会在实例化对象上建立对应的副本,同时进行写操作,此时这个实例也覆盖了原型链上的这个属性和方法。这种方法很好的保护了原型链上节点的共享性,不用担心不小心被其他实例所修改。这样大大地节省了内存,可以在频繁的对象创建中,有效地减少了内存空间的使用。
当然可以通过函数b上的prototype对象来修改链上的属性与方法,这样就可以做到一改全改,让子类共享的属性发生改变。如上面的 b.prototype.ia=3;操作。
这种做法也衍生出一种叫做延时绑定的思想,在使用的时侯才进行属性和方法的绑定,这种行为是可以通过原型链、prototype这个属性实现的。这样可以很方便地应用到你的编程中,但是注意,这种方法还是弊大于利的,因为可以在任何地方进行重绑定。所以程序者也许不知道在何时进行了绑定,使得代码很混乱,同时在大代码量的情况下,照成不可预料的结果。因此,延时绑定必须慎用。
四、深入剖析原型链。
上面说到,原型链是由prototype连在一起的,事实上这种说法是不严谨,甚至可以说是错误的,因为prototype是一个对象,并且实例是没有prototype这个属性的,但是,有比prototype更好的,用来表现原型链的东西,这是对象实例后的一个私有属性,ie下是不可访问的,但是用firefox是可以查看的,它才是真正的作为实例在原型链上查找属性与方法的一个类似指针的属性。可以这么说,prototype是函数所有的,而__proto__是实例化的对象存在的属性。我们常把这个__proto__指向的原型成为父原型。因为prototype是一个对象的实例,所以原型也有父原型。下面给出一段例子来解释这个现象。
function a()
{
this.ia=1;
this.get=function()
{
return this.ia;
}
}
function b() //b构造函数
{
this.ic=10;
this.getfatherclass=function()
{
return this.ic;
}
}
b.prototype=new a();//继承实例。
alert(a instanceof Function);//a是Function的实例;
alert(a.__proto__ === Function.prototype);//a的父原型指向到Function的原型;
alert(b.__proto__ === Function.prototype);//a的父原型指向到Function的原型
var obj =new b();//实例化函数b
alert(typeof(obj.__proto__));//输出obejct,说明是一个对象
alert(obj instanceof Object);//ture.obj是其中一个对象
alert(obj.__proto__===b.prototype);//ture。因为__prototype指向父原型
alert(obj.__proto__.__proto__===a.prototype);//ture。因为__prototype指向父原型
alert(obj.__proto__.__proto__.__proto__===Object.prototype);//ture。因为__prototype指向父原型
alert(Object.prototype.__proto__);//null。因为Object的原型是所有父原型的顶端,它不再具有父原型;
alert(Function instanceof Object);//Function是Object的实例;
alert(Function.__proto__ === Function.prototype);//Function的父原型指向到Function的原型;
alert(Function.prototype.__proto__ === Object.prototype);//Function的原型的父原型指向到Object的原型
alert(Object.__proto__ === Function.prototype);//Object的父原型指向到Function的原型;
从上面的例子我们再也清晰不过了,实例查找原型链的时候使用的__proto__ 这个私有属性,通过这个属性一层一层地遍历原型链,而原型链的最终父原型是Object.prototype,它是所有实例的父原型,只要修改这个父原型,那么所有的实例都将用到修改的属性。
同时。Function.prototype是所有函数的父原型,而它Function.prototype的父原型也是Object.prototype。到了这里,我们可以清晰地看到一个实例后面的原型链的全貌了。