JavaScript 继承详解(五)
在本章中,我们将分析JohnResig关于JavaScript继承的一个实现- SimpleJavaScriptInheritance。
JohnResig作为jQuery的创始人而声名在外。是《ProJavaScriptTechniques》的作者,而且Resig将会在今年秋天推出一本书《JavaScriptSecrets》,非常期待。
调用方式
调用方式非常优雅:
注意:代码中的Class、extend、_super都是自定义的对象,我们会在后面的代码分析中详解。
varPerson=Class.extend({
//init是构造函数
init:function(name){
this.name=name;
},
getName:function(){
returnthis.name;
}
});
//Employee类从Person类继承
varEmployee=Person.extend({
//init是构造函数
init:function(name,employeeID){
//在构造函数中调用父类的构造函数
this._super(name);
this.employeeID=employeeID;
},
getEmployeeID:function(){
returnthis.employeeID;
},
getName:function(){
//调用父类的方法
return"Employeename:"+this._super();
}
});
varzhang=newEmployee("ZhangSan","1234");
console.log(zhang.getName());//"Employeename:ZhangSan"
说实话,对于完成本系列文章的目标-继承-而言,真找不到什么缺点。方法一如jQuery一样简洁明了。
代码分析
为了一个漂亮的调用方式,内部实现的确复杂了很多,不过这些也是值得的-一个人的思考带给了无数程序员快乐的微笑-嘿嘿,有点肉麻。
不过其中的一段代码的确迷惑我一段时间:
fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;
我曾在几天前的博客中写过一篇文章专门阐述这个问题,有兴趣可以向前翻一翻。
//自执行的匿名函数创建一个上下文,避免引入全局变量
(function(){
//initializing变量用来标示当前是否处于类的创建阶段,
//-在类的创建阶段是不能调用原型方法init的
//-我们曾在本系列的第三篇文章中详细阐述了这个问题
//fnTest是一个正则表达式,可能的取值为(/\b_super\b/或/.*/)
//-对/xyz/.test(function(){xyz;})的测试是为了检测浏览器是否支持test参数为函数的情况
//-不过我对IE7.0,Chrome2.0,FF3.5进行了测试,此测试都返回true。
//-所以我想这样对fnTest赋值大部分情况下也是对的:fnTest=/\b_super\b/;
varinitializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;
//基类构造函数
//这里的this是window,所以这整段代码就向外界开辟了一扇窗户-window.Class
this.Class=function(){};
//继承方法定义
Class.extend=function(prop){
//这个地方很是迷惑人,还记得我在本系列的第二篇文章中提到的么
//-this具体指向什么不是定义时能决定的,而是要看此函数是怎么被调用的
//-我们已经知道extend肯定是作为方法调用的,而不是作为构造函数
//-所以这里this指向的不是Object,而是Function(即是Class),那么this.prototype就是父类的原型对象
//-注意:_super指向父类的原型对象,我们会在后面的代码中多次碰见这个变量
var_super=this.prototype;
//通过将子类的原型指向父类的一个实例对象来完成继承
//-注意:this是基类构造函数(即是Class)
initializing=true;
varprototype=newthis();
initializing=false;
//我觉得这段代码是经过作者优化过的,所以读起来非常生硬,我会在后面详解
for(varnameinprop){
prototype[name]=typeofprop[name]=="function"&&
typeof_super[name]=="function"&&fnTest.test(prop[name])?
(function(name,fn){
returnfunction(){
vartmp=this._super;
this._super=_super[name];
varret=fn.apply(this,arguments);
this._super=tmp;
returnret;
};
})(name,prop[name]):
prop[name];
}
//这个地方可以看出,Resig很会伪装哦
//-使用一个同名的局部变量来覆盖全局变量,很是迷惑人
//-如果你觉得拗口的话,完全可以使用另外一个名字,比如functionF()来代替functionClass()
//-注意:这里的Class不是在最外层定义的那个基类构造函数
functionClass(){
//在类的实例化时,调用原型方法init
if(!initializing&&this.init)
this.init.apply(this,arguments);
}
//子类的prototype指向父类的实例(完成继承的关键)
Class.prototype=prototype;
//修正constructor指向错误
Class.constructor=Class;
//子类自动获取extend方法,arguments.callee指向当前正在执行的函数
Class.extend=arguments.callee;
returnClass;
};
})();
下面我会对其中的for-in循环进行解读,把自执行的匿名方法用一个局部函数来替换,这样有利于我们看清真相:
(function(){
varinitializing=false,fnTest=/xyz/.test(function(){xyz;})?/\b_super\b/:/.*/;
this.Class=function(){};
Class.extend=function(prop){
var_super=this.prototype;
initializing=true;
varprototype=newthis();
initializing=false;
//如果父类和子类有同名方法,并且子类中此方法(name)通过_super调用了父类方法
//-则重新定义此方法
functionfn(name,fn){
returnfunction(){
//将实例方法_super保护起来。
//个人觉得这个地方没有必要,因为每次调用这样的函数时都会对this._super重新定义。
vartmp=this._super;
//在执行子类的实例方法name时,添加另外一个实例方法_super,此方法指向父类的同名方法
this._super=_super[name];
//执行子类的方法name,注意在方法体内this._super可以调用父类的同名方法
varret=fn.apply(this,arguments);
this._super=tmp;
//返回执行结果
returnret;
};
}
//拷贝prop中的所有属性到子类原型中
for(varnameinprop){
//如果prop和父类中存在同名的函数,并且此函数中使用了_super方法,则对此方法进行特殊处理-fn
//否则将此方法prop[name]直接赋值给子类的原型
if(typeofprop[name]==="function"&&
typeof_super[name]==="function"&&fnTest.test(prop[name])){
prototype[name]=fn(name,prop[name]);
}else{
prototype[name]=prop[name];
}
}
functionClass(){
if(!initializing&&this.init){
this.init.apply(this,arguments);
}
}
Class.prototype=prototype;
Class.constructor=Class;
Class.extend=arguments.callee;
returnClass;
};
})();
写到这里,大家是否觉得Resig的实现和我们在第三章一步一步实现的jClass很类似。其实在写这一系列的文章之前,我已经对prototype、mootools、extjs、jQuery-Simple-Inheritance、Crockford-Classical-Inheritance这些实现有一定的了解,并且大部分都在实际项目中使用过。在第三章中实现jClass也参考了Resig的实现,在此向Resig表示感谢。
下来我们就把jClass改造成和这里的Class具有相同的行为。
我们的实现
将我们在第三章实现的jClass改造成目前JohnResig所写的形式相当简单,只需要修改其中的两三行就行了:
(function(){
//当前是否处于创建类的阶段
varinitializing=false;
jClass=function(){};
jClass.extend=function(prop){
//如果调用当前函数的对象(这里是函数)不是Class,则是父类
varbaseClass=null;
if(this!==jClass){
baseClass=this;
}
//本次调用所创建的类(构造函数)
functionF(){
//如果当前处于实例化类的阶段,则调用init原型函数
if(!initializing){
//如果父类存在,则实例对象的baseprototype指向父类的原型
//这就提供了在实例对象中调用父类方法的途径
if(baseClass){
this._superprototype=baseClass.prototype;
}
this.init.apply(this,arguments);
}
}
//如果此类需要从其它类扩展
if(baseClass){
initializing=true;
F.prototype=newbaseClass();
F.prototype.constructor=F;
initializing=false;
}
//新创建的类自动附加extend函数
F.extend=arguments.callee;
//覆盖父类的同名函数
for(varnameinprop){
if(prop.hasOwnProperty(name)){
//如果此类继承自父类baseClass并且父类原型中存在同名函数name
if(baseClass&&
typeof(prop[name])==="function"&&
typeof(F.prototype[name])==="function"&&
/\b_super\b/.test(prop[name])){
//重定义函数name-
//首先在函数上下文设置this._super指向父类原型中的同名函数
//然后调用函数prop[name],返回函数结果
//注意:这里的自执行函数创建了一个上下文,这个上下文返回另一个函数,
//此函数中可以应用此上下文中的变量,这就是闭包(Closure)。
//这是JavaScript框架开发中常用的技巧。
F.prototype[name]=(function(name,fn){
returnfunction(){
this._super=baseClass.prototype[name];
returnfn.apply(this,arguments);
};
})(name,prop[name]);
}else{
F.prototype[name]=prop[name];
}
}
}
returnF;
};
})();
//经过改造的jClass
varPerson=jClass.extend({
init:function(name){
this.name=name;
},
getName:function(prefix){
returnprefix+this.name;
}
});
varEmployee=Person.extend({
init:function(name,employeeID){
//调用父类的方法
this._super(name);
this.employeeID=employeeID;
},
getEmployeeIDName:function(){
//注意:我们还可以通过这种方式调用父类中的其他函数
varname=this._superprototype.getName.call(this,"Employeename:");
returnname+",EmployeeID:"+this.employeeID;
},
getName:function(){
//调用父类的方法
returnthis._super("Employeename:");
}
});
varzhang=newEmployee("ZhangSan","1234");
console.log(zhang.getName());//"Employeename:ZhangSan"
console.log(zhang.getEmployeeIDName());//"Employeename:ZhangSan,EmployeeID:1234"
这篇文章就接受到这了,下面还有一个系列的文章,大家都可以看下