JavaScript 继承详解(六)
在本章中,我们将分析Prototypejs中关于JavaScript继承的实现。
Prototypejs是最早的JavaScript类库,可以说是JavaScript类库的鼻祖。我在几年前接触的第一个JavaScript类库就是这位,因此Prototypejs有着广泛的群众基础。
不过当年Prototypejs中的关于继承的实现相当的简单,源代码就寥寥几行,我们来看下。
早期Prototypejs中继承的实现
源码:
varClass={
//Class.create仅仅返回另外一个函数,此函数执行时将调用原型方法initialize
create:function(){
returnfunction(){
this.initialize.apply(this,arguments);
}
}
};
//对象的扩展
Object.extend=function(destination,source){
for(varpropertyinsource){
destination[property]=source[property];
}
returndestination;
};
调用方式:
varPerson=Class.create();
Person.prototype={
initialize:function(name){
this.name=name;
},
getName:function(prefix){
returnprefix+this.name;
}
};
varEmployee=Class.create();
Employee.prototype=Object.extend(newPerson(),{
initialize:function(name,employeeID){
this.name=name;
this.employeeID=employeeID;
},
getName:function(){
return"Employeename:"+this.name;
}
});
varzhang=newEmployee("ZhangSan","1234");
console.log(zhang.getName());//"Employeename:ZhangSan"
很原始的感觉对吧,在子类函数中没有提供调用父类函数的途径。
Prototypejs1.6以后的继承实现
首先来看下调用方式:
//通过Class.create创建一个新类
varPerson=Class.create({
//initialize是构造函数
initialize:function(name){
this.name=name;
},
getName:function(prefix){
returnprefix+this.name;
}
});
//Class.create的第一个参数是要继承的父类
varEmployee=Class.create(Person,{
//通过将子类函数的第一个参数设为$super来引用父类的同名函数
//比较有创意,不过内部实现应该比较复杂,至少要用一个闭包来设置$super的上下文this指向当前对象
initialize:function($super,name,employeeID){
$super(name);
this.employeeID=employeeID;
},
getName:function($super){
return$super("Employeename:");
}
});
varzhang=newEmployee("ZhangSan","1234");
console.log(zhang.getName());//"Employeename:ZhangSan"
这里我们将Prototypejs1.6.0.3中继承实现单独取出来,那些不想引用整个prototype库而只想使用prototype式继承的朋友,可以直接把下面代码拷贝出来保存为JS文件就行了。
varPrototype={
emptyFunction:function(){}
};
varClass={
create:function(){
varparent=null,properties=$A(arguments);
if(Object.isFunction(properties[0]))
parent=properties.shift();
functionklass(){
this.initialize.apply(this,arguments);
}
Object.extend(klass,Class.Methods);
klass.superclass=parent;
klass.subclasses=[];
if(parent){
varsubclass=function(){};
subclass.prototype=parent.prototype;
klass.prototype=newsubclass;
parent.subclasses.push(klass);
}
for(vari=0;i<properties.length;i++)
klass.addMethods(properties[i]);
if(!klass.prototype.initialize)
klass.prototype.initialize=Prototype.emptyFunction;
klass.prototype.constructor=klass;
returnklass;
}
};
Class.Methods={
addMethods:function(source){
varancestor=this.superclass&&this.superclass.prototype;
varproperties=Object.keys(source);
if(!Object.keys({toString:true}).length)
properties.push("toString","valueOf");
for(vari=0,length=properties.length;i<length;i++){
varproperty=properties[i],value=source[property];
if(ancestor&&Object.isFunction(value)&&value.argumentNames().first()=="$super"){
varmethod=value;
value=(function(m){
returnfunction(){returnancestor[m].apply(this,arguments)};
})(property).wrap(method);
value.valueOf=method.valueOf.bind(method);
value.toString=method.toString.bind(method);
}
this.prototype[property]=value;
}
returnthis;
}
};
Object.extend=function(destination,source){
for(varpropertyinsource)
destination[property]=source[property];
returndestination;
};
function$A(iterable){
if(!iterable)return[];
if(iterable.toArray)returniterable.toArray();
varlength=iterable.length||0,results=newArray(length);
while(length--)results[length]=iterable[length];
returnresults;
}
Object.extend(Object,{
keys:function(object){
varkeys=[];
for(varpropertyinobject)
keys.push(property);
returnkeys;
},
isFunction:function(object){
returntypeofobject=="function";
},
isUndefined:function(object){
returntypeofobject=="undefined";
}
});
Object.extend(Function.prototype,{
argumentNames:function(){
varnames=this.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g,'').split(',');
returnnames.length==1&&!names[0]?[]:names;
},
bind:function(){
if(arguments.length<2&&Object.isUndefined(arguments[0]))returnthis;
var__method=this,args=$A(arguments),object=args.shift();
returnfunction(){
return__method.apply(object,args.concat($A(arguments)));
}
},
wrap:function(wrapper){
var__method=this;
returnfunction(){
returnwrapper.apply(this,[__method.bind(this)].concat($A(arguments)));
}
}
});
Object.extend(Array.prototype,{
first:function(){
returnthis[0];
}
});
首先,我们需要先解释下Prototypejs中一些方法的定义。
argumentNames:获取函数的参数数组
functioninit($super,name,employeeID){
console.log(init.argumentNames().join(","));//"$super,name,employeeID"
}
bind:绑定函数的上下文this到一个新的对象(一般是函数的第一个参数)
varname="window";
varp={
name:"Lisi",
getName:function(){
returnthis.name;
}
};
console.log(p.getName());//"Lisi"
console.log(p.getName.bind(window)());//"window"
wrap:把当前调用函数作为包裹器wrapper函数的第一个参数
varname="window";
varp={
name:"Lisi",
getName:function(){
returnthis.name;
}
};
functionwrapper(originalFn){
return"Hello:"+originalFn();
}
console.log(p.getName());//"Lisi"
console.log(p.getName.bind(window)());//"window"
console.log(p.getName.wrap(wrapper)());//"Hello:window"
console.log(p.getName.wrap(wrapper).bind(p)());//"Hello:Lisi"
有一点绕口,对吧。这里要注意的是wrap和bind调用返回的都是函数,把握住这个原则,就很容易看清本质了。
对这些函数有了一定的认识之后,我们再来解析Prototypejs继承的核心内容。
这里有两个重要的定义,一个是Class.extend,另一个是Class.Methods.addMethods。
varClass={
create:function(){
//如果第一个参数是函数,则作为父类
varparent=null,properties=$A(arguments);
if(Object.isFunction(properties[0]))
parent=properties.shift();
//子类构造函数的定义
functionklass(){
this.initialize.apply(this,arguments);
}
//为子类添加原型方法Class.Methods.addMethods
Object.extend(klass,Class.Methods);
//不仅为当前类保存父类的引用,同时记录了所有子类的引用
klass.superclass=parent;
klass.subclasses=[];
if(parent){
//核心代码-如果父类存在,则实现原型的继承
//这里为创建类时不调用父类的构造函数提供了一种新的途径
//-使用一个中间过渡类,这和我们以前使用全局initializing变量达到相同的目的,
//-但是代码更优雅一点。
varsubclass=function(){};
subclass.prototype=parent.prototype;
klass.prototype=newsubclass;
parent.subclasses.push(klass);
}
//核心代码-如果子类拥有父类相同的方法,则特殊处理,将会在后面详解
for(vari=0;i<properties.length;i++)
klass.addMethods(properties[i]);
if(!klass.prototype.initialize)
klass.prototype.initialize=Prototype.emptyFunction;
//修正constructor指向错误
klass.prototype.constructor=klass;
returnklass;
}
};
再来看addMethods做了哪些事情:
Class.Methods={
addMethods:function(source){
//如果父类存在,ancestor指向父类的原型对象
varancestor=this.superclass&&this.superclass.prototype;
varproperties=Object.keys(source);
//Firefox和Chrome返回1,IE8返回0,所以这个地方特殊处理
if(!Object.keys({toString:true}).length)
properties.push("toString","valueOf");
//循环子类原型定义的所有属性,对于那些和父类重名的函数要重新定义
for(vari=0,length=properties.length;i<length;i++){
//property为属性名,value为属性体(可能是函数,也可能是对象)
varproperty=properties[i],value=source[property];
//如果父类存在,并且当前当前属性是函数,并且此函数的第一个参数为$super
if(ancestor&&Object.isFunction(value)&&value.argumentNames().first()=="$super"){
varmethod=value;
//下面三行代码是精华之所在,大概的意思:
//-首先创建一个自执行的匿名函数返回另一个函数,此函数用于执行父类的同名函数
//-(因为这是在循环中,我们曾多次指出循环中的函数引用局部变量的问题)
//-其次把这个自执行的匿名函数的作为method的第一个参数(也就是对应于形参$super)
//不过,窃以为这个地方作者有点走火入魔,完全没必要这么复杂,后面我会详细分析这段代码。
value=(function(m){
returnfunction(){returnancestor[m].apply(this,arguments)};
})(property).wrap(method);
value.valueOf=method.valueOf.bind(method);
//因为我们改变了函数体,所以重新定义函数的toString方法
//这样用户调用函数的toString方法时,返回的是原始的函数定义体
value.toString=method.toString.bind(method);
}
this.prototype[property]=value;
}
returnthis;
}
};
上面的代码中我曾有“走火入魔”的说法,并不是对作者的亵渎,只是觉得作者对JavaScript中的一个重要准则(通过自执行的匿名函数创建作用域)运用的有点过头。
value=(function(m){
returnfunction(){returnancestor[m].apply(this,arguments)};
})(property).wrap(method);
其实这段代码和下面的效果一样:
value=ancestor[property].wrap(method);
我们把wrap函数展开就能看的更清楚了:
value=(function(fn,wrapper){
var__method=fn;
returnfunction(){
returnwrapper.apply(this,[__method.bind(this)].concat($A(arguments)));
}
})(ancestor[property],method);
可以看到,我们其实为父类的函数ancestor[property]通过自执行的匿名函数创建了作用域。而原作者是为property创建的作用域。两则的最终效果是一致的。
我们对Prototypejs继承的重实现
分析了这么多,其实也不是很难,就那么多概念,大不了换种表现形式。
下面我们就用前几章我们自己实现的jClass来实现Prototypejs形式的继承。
//注意:这是我们自己实现的类似Prototypejs继承方式的代码,可以直接拷贝下来使用
//这个方法是借用Prototypejs中的定义
functionargumentNames(fn){
varnames=fn.toString().match(/^[\s\(]*function[^(]*\(([^\)]*)\)/)[1].replace(/\s+/g,'').split(',');
returnnames.length==1&&!names[0]?[]:names;
}
functionjClass(baseClass,prop){
//只接受一个参数的情况-jClass(prop)
if(typeof(baseClass)==="object"){
prop=baseClass;
baseClass=null;
}
//本次调用所创建的类(构造函数)
functionF(){
//如果父类存在,则实例对象的baseprototype指向父类的原型
//这就提供了在实例对象中调用父类方法的途径
if(baseClass){
this.baseprototype=baseClass.prototype;
}
this.initialize.apply(this,arguments);
}
//如果此类需要从其它类扩展
if(baseClass){
varmiddleClass=function(){};
middleClass.prototype=baseClass.prototype;
F.prototype=newmiddleClass();
F.prototype.constructor=F;
}
//覆盖父类的同名函数
for(varnameinprop){
if(prop.hasOwnProperty(name)){
//如果此类继承自父类baseClass并且父类原型中存在同名函数name
if(baseClass&&
typeof(prop[name])==="function"&&
argumentNames(prop[name])[0]==="$super"){
//重定义子类的原型方法prop[name]
//-这里面有很多JavaScript方面的技巧,如果阅读有困难的话,可以参阅我前面关于JavaScriptTipsandTricks的系列文章
//-比如$super封装了父类方法的调用,但是调用时的上下文指针要指向当前子类的实例对象
//-将$super作为方法调用的第一个参数
F.prototype[name]=(function(name,fn){
returnfunction(){
varthat=this;
$super=function(){
returnbaseClass.prototype[name].apply(that,arguments);
};
returnfn.apply(this,Array.prototype.concat.apply($super,arguments));
};
})(name,prop[name]);
}else{
F.prototype[name]=prop[name];
}
}
}
returnF;
};
调用方式和Prototypejs的调用方式保持一致:
varPerson=jClass({
initialize:function(name){
this.name=name;
},
getName:function(){
returnthis.name;
}
});
varEmployee=jClass(Person,{
initialize:function($super,name,employeeID){
$super(name);
this.employeeID=employeeID;
},
getEmployeeID:function(){
returnthis.employeeID;
},
getName:function($super){
return"Employeename:"+$super();
}
});
varzhang=newEmployee("ZhangSan","1234");
console.log(zhang.getName());//"Employeename:ZhangSan"
经过本章的学习,就更加坚定了我们的信心,像Prototypejs形式的继承我们也能够轻松搞定。
以后的几个章节,我们会逐步分析mootools,Extjs等JavaScript类库中继承的实现,敬请期待。