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类库中继承的实现,敬请期待。