《仔仔细细分析Ext》 第一章 必须理解Ext.extend函数
《仔仔细细分析Ext》 第一章 必须理解Ext.extend函数
原创作者: damoqiongqiu 阅读:1362次 评论:11条 更新时间:2009-05-31 收藏
显然了,从函数名就可以看出来,这个函数是整个Ext类库的基石,之一。
笔者初看这个函数,感觉比较迷糊,只知道它完成了继承的功能,但是里面到底做了什么操作,并不太好理解。
网络上有很多帖子和文章来解析这个函数,但笔者感觉并不是很到位。不知道是因为这些帖子的作者没有能完全理解这个函数还是因为表达得不够清晰。
下面笔者就自己的理解来分析一下这个函数。
必须的前置概念有三个:prototype、constructor、“闭包”
没有这三个概念的请务必先看第1、2、3段代码,很了解的直接看第4段代码就可以了。
1、prototype
RectAngle=function (width,height){
this.width=width;
this.height=height;
}
RectAngle.prototype.area=function(){
return this.width*this.height;
}
这段代码似曾相识吧?来自《JavaScript权威指南》。功能很简单的啦,定义个“矩形”的构造函数,有长和宽两个参数。
然后在RectAngle的prototype里面增加一个计算面积的函数area.
这样每次在var rect=new RectAngle()的时候,就可以对rect对象调用area()函数了,因为rect对象从RectAngle的prototype里面继承了area()函数。
这就是“JavaScript基于原型继承”的简单理解。
2、constructor
根据《JavaScript权威指南》上面的解释,每个函数都有一个prototype属性,构造函数也是函数,所以也有prototype属性。prototype属性在定义函数的时候会自动创建并初始化.也就是说,在写下RectAngle=function(widht,height){//...}的时候,RectAngle的prototype属性就已经被创建了,这个时候prototype里面只有一个属性,它就是constructor(构造器),这个constructor指回了RectAngle函数本身。这样就形成了一个圈一样的链条,可以实验一下这种调用:
RectAngle.prototype.constructor.prototype.constructor...这个调用是比较变态的咯,如果你能看懂,你肯定琢磨过这个问题,呵呵。笔者也是琢磨了比较长的时间才明白其中的含义的。
当然,不明白这种变态写法也没关系的,毕竟每个哪个变态的人会在实际应用的时候写这种东西。
言归正传,对于每个RectAngle的实例来说,例如var rect=new RectAngle(10,10); rect.prototype会指向构造函数RectAngle的prototype,也就是说所有的实例都会共享同一份RectAngle.prototype,
如此,就不需要分配那么多内存给每个实例来存储prototype属性了。
3、“闭包”(代码来自《JavaScript权威指南》):
RectAngle=function(width,height){
this.getWidth=function(){return width};
this.getHeight=function(){return height};
}
RectAngle.prototype.area=function(){
return this.getWidth()*this.getHeight();
}
发现了吧?这段代码和第1段的构造函数是不同的.从RectAngle.prototype.area这个函数也可以看出来,除了RectAngle构造函数内部,外部函数无法直接访问RectAngle的width和height属性,只能通过执行getWidth()和getHeight()方法来获得这两个属性的值。
《指南》上面说,第一个发现这种写法的人是Douglas Crockford,呵呵,真是个变态的家伙,这都能想出来!无语啊,人和人是有差距的。(笔者的名言)
有了这种写法,就可以动态构建出功能强大的代码了,这种写法的用处是比较多的,例如像缓存调用变量、改变命名空间、定义私有属性等。依次来解释一下这三个用处:
⑴定义私有属性:从上面的代码可以看出来,外部函数是没有办法直接引用width和height这两个属性的,比如var rect=new RectAngle(widht,height);rect.weidth??
这么写就不行了。所以,通过RectAngle构造器中this.getWidth()方法就模拟出了一个私有的变量(因为JavaScript没有private这个说法,所以只能叫模拟哦)。
⑵改变命名空间:
例如把上面的代码写成这样:
RectAngle=function(width,height){
getWidth:function(){
var haha=function(){
return width;
}
return haha;
},
getHeight:function(){
var haha=function(){
return height;
}
return haha;
}
}
同样是可以运行的,看出来没有,两个get函数里面实际上用了同样名称的方法haha(),但是没有关系,它们的命名空间是不同的,一个处于getWidth的作用域,一个处于getHeight的作用域。当然在外部调用getWidth()方法的时候,实际运行的是里面对应的haha()方法。
⑶缓存变量:
与Java或者C++的作用域概念类似,一个方法中局部变量(方法的参数也可以看成是局部变量的一种),在方法运行完之后就会实效并释放内存。
例如var rect=new RectAngle(width,height);按理说,在构造函数执行完毕之后,width和height这两个变量就应该释放内存了,但是通过类似这种this.getWidth=function(){return width}的定义,width和height变量并不释放内存,否则在外部调用getWidht()的时候,就无法返回对应对象的width值了。
(“闭包”是稍微复杂的概念,在很多的脚本语言里面都有这个特性,JAVA中目前是没有这个概念,据说JAVA7将会添加“闭包”特性。但是笔者认为,作为一种重量级的语言,并不是什么特性都要有,像“闭包”这样的东西,在重量级语言里面,稍有不慎“内存泄露”起来是so easy的!如果写得再变态一点,很多局部变量都可以“逃出作用域”,变成内存孤岛(没有函数可以释放它,只能看着它干瞪眼)。
4、好了,有了上面的简单解释,可以来分析Ext的extend这个函数了。
首先还是把《指南》里面的继承的例子说一下,以便于理解(你很熟悉?跳过吧。)
RectAngle=function(w,h){
this.w=w;
this.h=h;
}
RectAngle.prototype.area=function(){
return this.w*this.h;
}
写个子类来继承RectAngle,这个子类叫做有颜色的矩形ColoredRectAngle,多一个color属性。
ColoredRectAngle=function(color,w,h){
RectAngle.call(this,w,h);
this.c=color;
}
上面已经把w和h属性拷贝到子类中来了,父类的prototype里面还有个area方法也得想办法拷贝进来,注意了,这是精彩的部分,不能错过哦。
ColoredRectAngle.prototype=new RectAngle();//这个写法其实包含了很多内容哦,我们把它拆开来写会更好理解
var rect=new RectAngle();
ColoredRectAngle.prototype=rect;//怎么样,含义是一样的吧?
好,开始分析这两句话。rect是RectAngle的实例(废话,它是由RectAngle构造函数构造出来的,当然是它的实例了!),但是
在构造rect的时候,没有传参数给它,这样的话在rect这个对象里面w和h这两个属性就是null(显然必须的)。
既然rect是RectAngle的实例,那么它的prototype会指向RectAngle.prototype,所以rect对象会拥有area()方法。
另外,rect.prototype.constructor指向的是RectAngle这个构造函数(显然必须的)。
好,现在ColoredRectAngle.prototype=rect,这一操作有三个问题,第一,rect的w和h被放到ColoredRectAngle.prototype里面来了,第二,rect.prototype.area()这个方法也到了ColoredRectAngle.prototype里面了,当然,完整的访问area()方法路径应该是ColoredRectAngle.prototype.prototype.area(),但是因为JavaScript的自动查找机制,放在prototype里面的属性会被自动找出来(加入从对象的直接属性里面找不到的话。)这样就没有必要写完整的访问路径了,直接写ColoredRectAngle.area()就可以找到area()了,看上去就好像ColoredRectAngle也拥有了area()方法。
值得注意的一点是,在执行RectAngle.call(this,w,h);这一步的时候我们已经把w和h两个属性拷贝到ColoredRectAngle里面了,这里我们不再需要rect里面这两个值为null的w和h,
所以,直接把它们删除了事,免得浪费内存。
Delete ColoredRectAngle.prototype.w;
delete ColoredRectAngle.prototype.h;
OK,到了这一步,看起来模拟继承的操作就算大功告成了,父类RectAngle的w和h属性通过RectAngle.call(this,w,h)拷贝进来了,父类prototype里面的方法也拷贝进来了,没用的废物(rect里面,也就是ColoredRectAngle.prototype里面,值为null的w和h)也剔除掉了。
看上去世界一片和谐。但是...还有一个暗藏的问题,请看:第三:这个时候ColoredRectAngle类的constructor指向错了。
本来,如果没有ColoredRectAngle.prototype=rect这步操作,ColoredRectAngle.prototype就是JavaScript自动创建出来的那个prototype,这个prototype有个constructor,指向了ColoredRectAngle构造函数自己.
但是,现在ColoredRectAngle.prototype=rect,如果现在来访问ColoredRectAngle.prototype.constructor,那么,根据自动查找机制,会找到rect.prototype.constructor,但这个constructor指向的是父类RectAngle构造函数,这个就不符合prototype的游戏规则了。因为,如果此时
var coloredRectAngle=new ColoredRectAngle('red',10,10) alert(coloredRectAngle.constructor);
得到的是父亲RectAngle的构造函数,从面向对象的观点看,这个结果是可以理解的,毕竟,子类对象也可以看成是父类对象。
但是,这样的话对于ColoredRectAngle的实例来说,就不能确切地知道它的constructor是ColoredRectAngle了。
所以,需要手动地把ColoredRectAngle.prototype.constructor设置回来。于是有了这一步:ColoredRectAngle.prototype.constructor=ColoredRectAngle.
OK,看完以上内容,如果你的意识仍然清醒,那就恭喜你了。否则,再仔细看看吧。
正式开始分析Ext.js里面Ext这个全局对象的extend方法。
完整的代码清单如下:
extend : function(){
// inline overrides
var io = function(o){
for(var m in o){
this[m] = o[m];
}
};
var oc = Object.prototype.constructor;
return function(sb, sp, overrides){
if(typeof sp == 'object'){
overrides = sp;
sp = sb;
sb = overrides.constructor != oc ? overrides.constructor : function(){sp.apply(this, arguments);};
}
var F = function(){}, sbp, spp = sp.prototype;
F.prototype = spp;
sbp = sb.prototype = new F();
sbp.constructor=sb;
sb.superclass=spp;
if(spp.constructor == oc){ spp.constructor=sp; }
sb.override = function(o){ Ext.override(sb, o); };
sbp.override = io;
Ext.override(sb, overrides);
sb.extend = function(o){Ext.extend(sb, o);};
return sb;
};
}()
首先,总体上看它是一个自执行函数,当Ext.js这个文件被浏览器加载的时候最外层的无参function就被执行。这个无参的function返回了一个有三个参数的function(sb,sp.overrides)。还记得上面的“闭包”吗?这种使用方式还是相当有创意的,Ext库里面存在大量类似的闭包写法。
var io = function(o){
for(var m in o){
this[m] = o[m];
}
};
这一段就不用解释了,是一个用来拷贝属性的普通函数。
var oc = Object.prototype.constructor;这句定义了一个变量oc,它的值是Object这个根类的constructor,大家可以把它alert出来看,
它是这样的:
function Object(){
[native code]
}
显然,JavaScript类库并不希望我们看到这个函数里面的实现,但是我们知道alert出来的这个东西就是JavaScript根类Object的构造函数。
来分析这个带有三个参数的闭包函数,
if(typeof sp == 'object'){
overrides = sp;
sp = sb;
sb = overrides.constructor != oc ? overrides.constructor : function(){sp.apply(this, arguments);};
}
单是这个if判断当时就让笔者郁闷了好久,呵呵,人和人真的是有差距的啊!
if(typeof sp == 'object')这个判断是干嘛的呢?呵呵,它是用来判断你传递进来参数的个数的。例如Ext.Panel = Ext.extend(Ext.Container, {...});
Ext类库里面基本都是传两个参数给extend方法,此时,这个if判断就要起作用啦。还不明白?硬是要说破啊。因为如果只传两个参数的话,在function(sb,sp,overrides)看来
第二个参数sp不就是个“object”麽?
sb = overrides.constructor != oc ? overrides.constructor : function(){sp.apply(this, arguments);};
这一句用来决定子类使用什么形式的构造函数,如果overrides里面有个constructor属性,就用overrides的constructor当作子类的构造函数。否则,创建个新的function出来,里面包含一句话,就是"sp.apply(this, arguments);",这个又是闭包的一个应用哦,在退出extend方法之后并没有释放局部变量sp的内存空间。
这样的话,每次new一个子类的时候,第一句执行的就是sp.apply(this,arguments);这个方法与《指南》里面RectAngle.call(this,w,h)完成的功能是一样的。就是把arguments全部拷贝到子类中去。
好了,属性拷贝完成之后就要拷贝父类prototype里面的方法了。来看看Ext又有什么精彩的写法:
var F = function(){}, sbp, spp = sp.prototype;
F.prototype = spp;
sbp = sb.prototype = new F();
sbp.constructor=sb;
这几句要连起来看哦。
按照前面《指南》里面的写法的话,应该是这样的:
第一步:把子类的prototype赋值为父类的实例对象。sbp=sb.prototype=new sp();
第二步:删除不要的废属性,因为前面的if判断里面sp.apply(this,arguments)已经完成了属性的拷贝。
第三步:把constructor重新手动指回来。sbp.constructor=sb
发现没有?如果采用《指南》里面的写法,必须要有第二步,把不要的属性都删除掉(不删会怎样?一个是可能会存在属性覆盖的问题,另外就是内存浪费了,当new出很多对象来的时候,这种浪费就很可观了哦!)。如果属性很多,岂不要写很多delete?而且要一个一个去核对一下超类里面的属性名称,显然Ext的作者并不希望这么做。于是有了这几句精彩的var F=function(){},定义一个空函数,里面没有属性。然后F.prototype=sp.prototype再然后sbp=new F()这么做的话,就把F.prototype也就是sp.prototype里面的东西拷贝到sb.prototype里面了,同时,因为F是个没有任何属性的函数,所以不需要再delete任何东西了。这句真的很精彩哦!
这时候sb.prototype.constructor是F(),所以再来一句sbp.constructor=sb。这样的话就完美地完成了对父类prototype的拷贝,而又没有把不要的属性拷进来。
到了这里,关健的两步操作:属性拷贝、方法拷贝(prototype里面的)都已经完成。
后面的代码就比较简单了,不再解释。
看完这篇文章你应该能理解这个核心的extend函数到底完成了什么操作了,如果还是不明白,我不得不承认,那还是我的错,那么请联系我吧QQ:253445528,注明“Ext源码分析”。说明:在5×8小时的上班时间不解答问题。
这篇文章耗费笔者近三个小时的时间,请尊重原创,转载请注明出处,谢谢。