dojo的类机制支持类声明、继承、调用父类方法等功能。dojo在底层实现上是通过操作原型链来实现其类机制的,而在实现继承时采用类式继承的方式。值得一提的是,dojo的类机制允许进行多重继承(注意,只有父类列表中的第一个作为真正的父类,其它的都是将其属性以mixin的方法加入到子类的原型链中),为解决多重继承时类方法的顺序问题,dojo用JavaScript实现了Python和其它多继承语言所支持的C3父类线性化算法,以实现线性的继承关系,想了解更多该算法的知识,可参考这里,我们在后面的分析中将会简单讲解dojo对此算法的实现。
1.dojo类声明概览
dojo类声明相关的代码位于“/dojo/_base/declare.js”文件中,定义类是通过dojo.declare方法来实现的。关于这个方法的基本用法,已经在dojo类机制简介这篇文章中进行了阐述,现在我们看一下它的实现原理(在这部分的代码分析中,会在整体上介绍dojo如何声明类,后文会对里面的重要细节内容进行介绍):
//此即为dojo.declare方法的定义 d.declare = function(className, superclass, props){ //前面有格式化参数相关的操作,一般情况下定义类会把三个参数全传进来,分别为 //类名、父类(可以为null、某个类或多个类组成的数组)和要声明类的属性及方法 //定义一系列的变量供后面使用 var proto, i, t, ctor, name, bases, chains, mixins = 1, parents = superclass; // 处理要声明类的父类 if(opts.call(superclass) == "[object Array]"){ //如果父类参数传过来的是数组,那么这里就是多继承,要用C3算法处理父类的关系 //得到的bases为数组,第一个元素能标识真正父类(即superclass参数中的第一个) //在数组中的索引,其余的数组元素是按顺序排好的继承链,后面还会介绍到C3算法 bases = c3mro(superclass, className); t = bases[0]; mixins = bases.length - t; superclass = bases[mixins]; }else{ //此分支内是对没有父类或单个父类情况的处理,不再详述 } //以下为构建类的原型属性和方法 if(superclass){ for(i = mixins - 1;; --i){ //此处遍历所有需要mixin的类 //注意此处,为什么说多个父类的情况下,只有第一个父类是真正的父类呢,因为在第一次循环的实例化了该父类,并记在了原型链中,而其它需要mixin的 //父类在后面处理时会把superclass设为一个空的构造方法,合并父类原型链 //后进行实例化 proto = forceNew(superclass); if(!i){ //此处在完成最后一个父类后跳出循环 break; } // mix in properties t = bases[i];//得到要mixin的一个父类 (t._meta ? mixOwn : mix)(proto, t.prototype);//合并原型链 // chain in new constructor ctor = new Function;//声明一个新的Function ctor.superclass = superclass; ctor.prototype = proto;//设置原型链 //此时将superclass指向了这个新的Function,再次进入这个循环的时候,实例//化的是ctor,而不是mixin的父类 superclass = proto.constructor = ctor; } }else{ proto = {}; } //此处将上面得到的方法(及属性)与要声明类本身所拥有的方法(及属性)进行合并 safeMixin(proto, props); ………… //此处收集链式调用相关的信息,后面会详述 for(i = mixins - 1; i; --i){ // intentional assignment t = bases[i]._meta; if(t && t.chains){ chains = mix(chains || {}, t.chains); } } if(proto["-chains-"]){ chains = mix(chains || {}, proto["-chains-"]); } //此处根据上面收集的链式调用信息和父类信息构建最终的构造方法,后文详述 t = !chains || !chains.hasOwnProperty(cname); bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) : (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t)); //在这个构造方法中添加了许多的属性,在进行链式调用以及调用父类方法等处会用到 ctor._meta = {bases: bases, hidden: props, chains: chains, parents: parents, ctor: props.constructor}; ctor.superclass = superclass && superclass.prototype; ctor.extend = extend; ctor.prototype = proto; proto.constructor = ctor; // 对于dojo.declare方法声明类的实例均有以下的工具方法 proto.getInherited = getInherited; proto.inherited = inherited; proto.isInstanceOf = isInstanceOf; // 此处要进行全局注册 if(className){ proto.declaredClass = className; d.setObject(className, ctor); } //对于链式调用父类的那些方法进行处理,实际上进行了重写,后文详述 if(chains){ for(name in chains){ if(proto[name] && typeof chains[name] == "string" && name != cname){ t = proto[name] = chain(name, bases, chains[name] === "after"); t.nom = name; } } } return ctor; // Function };
以上简单介绍了dojo声明类的整体流程,但是一些关键的细节如C3算法、链式调用在后面会继续进行介绍。
2.C3算法的实现
通过以前的文章和上面的分析,我们知道dojo的类声明支持多继承。在处理多继承时,不得不面对的就是继承链如何构造,比较现实的问题是如果多个父类都拥有同名的方法,那么在调用父类方法时,要按照什么规则确定调用哪个父类的呢?在解决这个问题上dojo实现了C3父类线性化的方法,对多个父类进行合理的排序,从而完美解决了这个问题。
为了了解继承链的相关知识,我们看一个简单的例子:
dojo.declare("A",null); dojo.declare("B",null); dojo.declare("C",null); dojo.declare("D",[A, B]); dojo.declare("E",[B, C]); dojo.declare("F",[A, C]); dojo.declare("G",[D, E]);
以上的代码中,声明了几个类,通过C3算法得到G的继承顺序应该是这样G->E->C->D->B->A的,只有按照这样的顺序才能保证类定义和依赖是正确的。那我们看一下这个C3算法是如何实现的呢:
function c3mro(bases, className){ //定义一系列的变量 var result = [], roots = [{cls: 0, refs: []}], nameMap = {}, clsCount = 1, l = bases.length, i = 0, j, lin, base, top, proto, rec, name, refs; //在这个循环中,构建出了父类各自的依赖关系(即父类可能会依赖其它的类) for(; i < l; ++i){ base = bases[i];//得到父类 ………… //在dojo声明的类中都有一个_meta属性,记录父类信息,此处能够得到包含本身在//内的继承链 lin = base._meta ? base._meta.bases : [base]; top = 0; for(j = lin.length - 1; j >= 0; --j){ //遍历继承链中的元素,注意,这里的处理是反向的,即从最底层的开始,一直到链的顶端 proto = lin[j].prototype; if(!proto.hasOwnProperty("declaredClass")){ proto.declaredClass = "uniqName_" + (counter++); } name = proto.declaredClass; // nameMap以map的方式记录了用到的类,不会重复 if(!nameMap.hasOwnProperty(name)){ //每个类都会有这样一个结构,其中refs特别重要,记录了引用了依赖类 nameMap[name] = {count: 0, refs: [], cls: lin[j]}; ++clsCount; } rec = nameMap[name]; if(top && top !== rec){ //满足条件时,意味着当前的类依赖此时top引用的类,即链的前一元素 rec.refs.push(top); ++top.count; } top = rec;//top指向当前的类,开始下一循环 } ++top.count; roots[0].refs.push(top);//在一个父类处理完成后就将它放在根的引用中 } //到此为止,我们建立了父类元素的依赖关系,以下要正确处理这些关系 while(roots.length){ top = roots.pop(); //将依赖的类插入到结果集中 result.push(top.cls); --clsCount; // optimization: follow a single-linked chain while(refs = top.refs, refs.length == 1){ //若当前类依赖的是一个父类,那处理这个依赖链 top = refs[0]; if(!top || --top.count){ //特别注意此时有一个top.count变量,是用来记录这个类被引用的次数, //如果减一之后,值还大于零,就说明后面还有引用,此时不做处理,这也就是 //在前面的例子中为什么不会出现G->E->C->B的原因 top = 0; break; } result.push(top.cls); --clsCount; } if(top){ //若依赖多个分支,则将依赖的类分别放到roots中,这段代码只有在多继承,//第一次进入时才会执行 for(i = 0, l = refs.length; i < l; ++i){ top = refs[i]; if(!--top.count){ roots.push(top); } } } } if(clsCount){ //如果上面处理完成后,clsCount的值还大于1,那说明出错了 err("can't build consistent linearization", className); } //构建完继承链后,要标识出真正父类在链的什么位置,就是通过返回数组的第一个元素 base = bases[0]; result[0] = base ? base._meta && base === result[result.length - base._meta.bases.length] ? base._meta.bases.length : 1 : 0; return result; }
通过以上的分析,我们可以看到,这个算法实现起来相当复杂,如果朋友们对其感兴趣,建议按照上文的例子,自己加断点进行调试分析。dojo的作者使用了不到100行的代码实现了这样强大的功能,里面有很多值得借鉴的设计思想。
3. 链式构造器的实现
在第一部分代码分析中我们曾经看到过定义构造函数的代码,如下:
bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) : (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t));
这个方法对与理解dojo类机制很重要。从前一篇文章的介绍中,我们了解到默认情况下,如果dojo声明的类存在继承关系,那么就会自动调用父类的构造方法,且是按照继承链的顺序先调用父类的构造方法,但是从1.4版本开始,dojo提供了手动设置构造方法调用的选项。在以上的代码中涉及到dojo声明类的三个方法,如果该类没有父类,那么调用的就是singleConstructor,如果有父类的话,那么默认调用的是chainedConstructor,如果手动设置了构造方法调用,那么调用的就是simpleConstructor,要启动这个选项只需在声明该类的时候添加chains的constructor声明即可。
比方说,我们在定义继承自com.levinzhang.Person的com.levinzhang.Employee类时,可以这样做:
dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{ "-chains-": { constructor:"manual" }, ………… }
添加以上代码后,在构造com.levinzhang.Employee实例时,就不会再调用所有父类的构造方法了,但是此时我们可以使用inherited方法显式的调用父类方法。
限于篇幅,以上的三个方法不全部介绍,只介绍chainedConstructor的核心实现:
function chainedConstructor(bases, ctorSpecial){ return function(){ //在此之前有一些准备工作,不详述了 //找到所有的父类,分别调用其构造方法 for(i = l - 1; i >= 0; --i){ f = bases[i]; m = f._meta; f = m ? m.ctor : f; //得到父类的构造方法 if(f){ //通过apply调用父类的方法 f.apply(this, preArgs ? preArgs[i] : a); } } // 请注意在构造方法执行完毕后,会执行名为postscript的方法,而这个方法是 //dojo的dijit组件实现的关键生命周期方法 f = this.postscript; if(f){ f.apply(this, args); } }; }
4. 调用父类方法的实现
在声明dojo类的时候,如果想调用父类的方法一般都是通过使用inherited方法来实现,但从1.4版本开始,dojo支持链式调用所有父类的方法,并引入了一些AOP的概念。我们将会分别介绍这两种方式。
通过inherited方式调用父类方法
在上一篇文章中,我们曾经介绍过,通过在类中使用inherited就可以调用到。这里我们要深入inherited的内部,看一下它的实现原理。因为inherited支持调用父类的一般方法和构造方法,两者略有不同,我们关注调用一般方法的过程。
function inherited(args, a, f){ ………… //在此之前有一些参数的处理 if(name != cname){ //不是构造方法 if(cache.c !== caller){ //在此之间的一些代码解决了确定调用者的问题,即确定从什么位置开始找父类 } //按照顺序找父类的同名方法 base = bases[++pos]; if(base){ proto = base.prototype; if(base._meta && proto.hasOwnProperty(name)){ f = proto[name];//找到此方法了 }else{ //如果没有找到对应的方法将按照继承链依次往前找 opf = op[name]; do{ proto = base.prototype; f = proto[name]; if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){ break; } }while(base = bases[++pos]); // intentional assignment } } f = base && f || op[name]; }else{ //此处是处理调用父类的构造方法 } if(f){ //方法找到后,执行 return a === true ? f : f.apply(this, a || args); } }
链式调用父类方法
这是从dojo 1.4版本新加入的功能。如果在执行某个方法时,也想按照一定的顺序执行父类的方法,只需在定义类时,在-chains-属性中加以声明即可。
dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{ "-chains-": { sayMyself: "before" }, …… }
添加了以上声明后,意味着Employee及其所有的子类,在调用sayMyself方法时,都会先调用本身的同名方法,然后再按照继承链依次调用所有父类的同名方法,我们还可以将值“before”替换为“after”,其执行顺序就会反过来。在-chains-属性中声明的方法,在类定义时,会进行特殊处理,正如我们在第一章中看到的那样:
if(chains){ for(name in chains){ if(proto[name] && typeof chains[name] == "string" && name != cname){ t = proto[name] = chain(name, bases, chains[name] === "after"); t.nom = name; } } }
我们可以看到,在-chains-中声明的方法都进行了替换,换成了chain方法的返回值,而这个方法也比较简单,源码如下:
function chain(name, bases, reversed){ return function(){ var b, m, f, i = 0, step = 1; if(reversed){ //判定顺序,即"after"还是"before",分别对应于循环的不同起点和方向 i = bases.length - 1; step = -1; } for(; b = bases[i]; i += step){ //按照顺序依次查找父类 m = b._meta; //找到父类中同名的方法 f = (m ? m.hidden : b.prototype)[name]; if(f){ //依次执行 f.apply(this, arguments); } } }; }
5.工具方法和属性如isInstanceOf、declaredClass的实现
除了上面提到的inherited方法以外,dojo在实现类功能的时候,还实现了一些工具方法和属性,这里介绍一个方法isInstanceOf和一个属性declaredClass。从功能上来说isInstanceOf方法用来判断一个对象是否为某个类的实例,而declaredClass属性得到的是某个对象所对应声明类的名字。
function isInstanceOf(cls){ //得到实例对象继承链上的所有类 var bases = this.constructor._meta.bases; //遍历所有的类,看是否与传进来的类相等 for(var i = 0, l = bases.length; i < l; ++i){ if(bases[i] === cls){ return true; } } return this instanceof cls; }
而declaredClass属性的实现比较简单,只是在声明类的原型上添加了一个属性而已,类的实例对象就可以访问这个属性得到其声明类的名字了。这段代码在dojo.declare方法中:
if(className){ proto.declaredClass = className; d.setObject(className, ctor); }
在dojo实现类机制的过程中,有一些内部的方法,是很值得借鉴的,如forceNew、safeMixin等,这些方法在实现功能的同时,保证了代码的高效执行,感兴趣的朋友可以进一步研究。
6.总结与思考
- dojo在实现类机制方面支持多继承方式,其它JavaScript类库中很少能做到,而利用JavaScript原生语法实现多继承也较为困难。在这一点上dojo的类机制的功能确实足够强大。但是多继承会增加编码的难度,对开发人员如何组织类也有更高的要求;
- 链式调用父类方法时,我们可以看到dojo引入了许多AOP的理念,在1.7的版本中,将会有单独的模块提供AOP相关的支持,我们将会持续关注相关的功能;
- 在dojo的代码中,多处都会出现方法替换,如链式方法调用、事件绑定等,这种设计思想值得我们关注和学习;
- 使用了许多的内部属性,如_meta、bases等,这些元数据在实现复杂的类机制中起到了至关重要的作用,在进行源码分析的时候,我们可以给予关注,如果要实现类似功能也可以进行借鉴。
探究类库的实现原理是提高自己编码水平的好办法,类似于dojo这样类库的核心代码基本上每一行都有其设计思想在里面(当然也不可以盲目崇拜),每次阅读和探索都会有所发现和心得,当然里面肯定也会有自以为是或谬误之处,在此很乐意和读到这篇文章的朋友们一起研究,欢迎批评指正。
参考资料:
关于作者
张卫滨,关注企业级Java开发和RIA技术,个人博客:http://lengyun3566.iteye.com,微博:http://weibo.com/zhangweibin1981
DOJO中的面向对象
在JS中并没有Java等面向对象语言中常见的类(class)的概念。也就是说,JS中的对象并非基于类的。它仅仅为开发者提供了一些原类型和基本的内置对象。从写法上来看,它更加的类似于面向过程式的语言,函数仿佛成了JS中的顶级实体。事实上,JS是一门函数式编程的语言。所以当我们需要以面向对象的方式来构建大型web应用时,原生态的JS并不能很好的满足这一点。而DOJO的出现完美的解决了这个问题,它使得程序员能够以传统的面向对象的思维来进行开发,进而使JS用起来更加得心应手。
第一章 JS中的对象模型
(一) 构造器(伪类)
在JS中创建一个对象很容易,JS提供了语法糖允许我们以字面量的方式来构建对象。例如:
- var foo={ 'name':'Jack' }
但是在JS中构建一个类却显得稍微复杂。由于JS并没有从语言层面上支持任何自定义类型,所以我们只能通过模拟的方式来构建出一个类。这得益于JS中的强大的函数以及原型机制。先来看一个简单的例子:
- function Foo(){
- this.name='Jack';
- }
- var foo=new Foo();
- console.log(foo.name) // 输出jack
在这个例子中,Foo已经不仅仅是一个简单的函数了,我们将Foo看成一个‘伪类’(在javascript中称为构造器)。因此可以用new运算符来生成该类型的对象。通常JS中的类型构造都采用该方法。新生成的对象将自动拥有‘伪类’中定义的字段,所以此例中生成的foo将拥有name属性。
注意Foo中的this.name='Jack'。由于JS中的某些不良设计,一般的函数调用会将该函数中的this绑定到全局对象上,这使得this的使用容易造成混乱。通常而言,如果并不涉及到面向对象编程,可以不必使用this。只有存在了某个对象,this的使用才会有意义。
如果对构造器进行new运算,构造器中的this会被绑定到生成的新的对象。换句话说,上例中new Foo()时,Foo中的this会被绑定到新生成的实例foo。可以猜测,对一个Foo调用new运算符的时候,会发生类似于下面的过程:
- var obj=new Object(); //obj是一个新生成的对象
- Foo.apply(obj); //将Foo中的this绑定到obj
- var foo=obj; //最后将obj引用返回给foo
(二) prototype是什么
JS中的继承有点特殊,在JS中并不存常见的基于类的继承。JS中的继承是对象与对象之间的行为。也就是说,一个对象可以直接继承另一个对象里的属性。而这一切,都是依靠prototype来完成的。示例如下:
- var foo={
- 'name':'Jack'
- }
- function Bar(age){
- this.age=age;
- }
- Bar.prototype=foo;
- var bar=new Bar(22);
这个例子中,我们首先创建了一个对象foo,它包含一个name属性。然后我们创建了一个构造器Bar,由于将Bar当做类来使用,所以将其首字母大写。随后我们将Bar的原型指向foo对象。接着,我们以new的方式来创建了一个Bar的实例bar。很显然,对象bar中包含了两个属性,name属性值为Jack,还有age属性,值为22。值得考究的是Bar. prototype=foo这一句。该语句将Bar的原型设定成一个对象foo。这一句的运行结果是通过Bar创建的所有对象都将继承foo对象的属性。于是,接下来bar便从foo中继承了name属性。
推广开来说, JS中的每个构造器都有一个prototype属性。JS里的构造器,除了包括了我们上面自己定义的‘伪类’,还包括了内置的Object、Function、String 、Array、Number、Date、RegExp等等一系列函数。prototype本身也是一个对象,也就是我们所说的原型对象,如果我们用构造器创建了一个新的对象,该对象便与原型对象发生了继承关系。注意,JS中的继承是发生在两个对象之间的关系,而JAVA之中的继承是两个类之间的关系。
JS中的继承有两个突出的特点,一是动态性,二是对修改封闭。下面的例子阐述了这两点,例一:
- var foo={ 'name':'Jack' }
- function Bar(age){
- this.age=age;
- }
- Bar.prototype=foo;
- var bar=new Bar(22);
- console.log(bar.name) //Jack
- foo.name='Mike';
- console.log(bar.name) //Mike
当我们修改了foo的属性时,通过bar来访问这些属性也会收到影响。也就是说,我们可以将一些需要的特性动态添加到JS的对象中。这是一种非常强大的编程技术。比如JS中的String对象缺少trim方法。通过
- String.prototype.trim=function(){//dojo中的实现
- return this.replace(/^ss*/,'').
- replace(/ss*$/,'');
- }
语句,可以为所有的string加上trim方法。
例二:
- var foo={ 'name':'Jack' }
- function Bar(age){
- this.age=age;
- }
- Bar.prototype=foo;
- var bar=new Bar(22);
- bar.name='Mike';
- console.log(bar.name) // Mike
- console.log(foo.name) // Jack
从上例中可以清楚的看出,如果我们试图通过修改bar来影响foo,这样是行不通的。通过bar可以访问foo的属性,但是却无法改变这些属性的值。当我们修改bar.name='Mike'之后,foo的name值依然是Jack。
(三) 原型链(prototype chain)
事实上,在bar对象中,有一个看不见的_proto属性。该属性指向了Bar.prototype,也就是foo。在Ecma-262 3rd Edition中有如下描述:
这段话的意思是JS中的构造器都拥有一个prototype属性。每个由构造器创建出来的object都含有一个指向该prototype的隐式引用。
- function Foo(){
- this.name='Jack';
- }
- var foo=new Foo();
- function Bar(age){
- this.age=age;
- }
- Bar.prototype=foo;
- var bar=new Bar(22);
因此,上例可以表示成:
注意绿色虚线框内的部分。通过_proto,可以将许多对象串起来,形成一个链条,也就是我们经常说的原型链(prototype chain)。当我们试图访问对象中的一个属性,首先会在对象本体中寻找该属性。如果没有找到,JS会自动在该对象的原型对象中查询该属性,这个过程是一种上溯。如果还是没有找到,会继续上溯到原型对象的原型对象中。
但是这个步骤不是无止尽的,这个上溯的过程直到Object.prototype._proto为止。以上面的图为例,从foo可以找到的原型对象是Foo.prototype。Foo.prototype本身是一个对象,也是Object的一个实例,因此有:Foo.prototype._proto=Object.prototype 。
所以在向上追溯的过程中,会追溯到Object.prototype这个对象。如果依然没有我们要找的属性,那还会继续向上追溯么?从Ecma-262 3rd Edition15.2.4节中可以得知:
也就是说Object.prototype._proto=null.至此,可以清楚的弄明白,整个原型链的最顶端的对象是Object.prototype,再往上就是null了。所以原型链可以认为是众多对象利用_proto串成的引用链,有点类似单链表,引用链的最后一个节点是Object.prototype。
(四) 维护constructor
只有当我们创建一个函数(JS中的函数也是对象)时,会自动为这个函数附上prototype对象, prototype中的所有属性会被遗传到该函数创建的对象上。在prototype的属性中,比较特殊的是constructor,constructor的值就是这个函数本身。赋上prototype对象的这个过程类似于:
- Foo.prototype={
- constructor:Foo
- }
如果我们执行:
- Foo.prototype.constructor===Foo
会输出true。同样如果在Bar.prototype = foo语句之前执行:
- Bar.prototype.constructor===Bar
也会输出true。由于在prototype对象中的属性会被继承,因此foo和bar中都能访问到constructor属性,分别指向Foo和Bar。
可以看出,JS中的constructor好比JAVA中的Class,在JAVA中一个对象可以通过getClass方法来获取自己的Class,那么JS中的对象可以直接访问constructor来获取自己的构造器。在多层次的继承问题上,我们可能需要维护正确的继承结构。由于无法访问_proto属性,因此继承链的维护只能依靠constructor属性。
- function Foo(){}
- var foo=new Foo();
- function Bar(){}
- Bar.prototype=foo;
- var bar=new Bar();
- console.log(bar.constructor) //Foo
运行上面这个例子,最后的输出结果为Foo。原因是foo中的constructor指向了Foo,而bar又从foo对象继承了该属性。这个时候需要进行一些修改,使得bar的constructor属性能够正确指向Bar。一般有两种方式来处理这个问题。一是在构造器里面修改constructor的值,第二种是在构造完成之后进行修改。
- function Foo(){}
- var foo=new Foo();
- function Bar(){
- this.constructor=Bar;
- }
- Bar.prototype=foo;
- var bar=new Bar();
- bar.constructor //Bar
- function Foo(){}
- var foo=new Foo();
- function Bar(){}
- Bar.prototype=foo;
- var bar=new Bar();
- bar.constructor= Bar;
如果使用下边一种方式,每次实例化一个Bar的对象,都要进行修改。推荐使用上边的方式来修改constructor属性,此修改会对所有Bar的实例生效。
DOJO中的面向对象__第二章 Dojo中的类
(一) 利用DOJO.DECLARE声明类
在第一章中讲到,JS提供的是一种基于原型(Prototype)的对象系统。这与JS诞生的初衷有关,在把JS仅仅用作页面脚本时,利用Prototype是一种优势。因为基于原型的对象不仅节约内存,初始化速度快,更重要的是还拥有了原型继承带来的动态特性。但是如果需要在RIA环境下构建丰富的web应用,JS的原型系统又显得不太给力。在继承性方面,原型系统能够让两个对象之间发生继承关系,但需要经过构造器来衔接。在封装性方面,JS的表现很糟糕,除了没有关键字支持,JS实质上也只有‘全局’和‘函数’两种级别的作用域。在多态性方面,由于缺少静态类型检查,JS对象无法准确识别子类或者父类的同名方法,更无法调用父类中已经定义的方法。所以归根结底的说,JS里采用的原型的对象系统并不是完备的面向对象系统。
对此,Dojo在面向对象的一些特性方面对JS进行了补充,使得开发者能够尽可能的利用Dojo以一种基于类的面向对象思想来组织构建程序。Dojo中提供了一种简单的类型声明方式——dojo.declare,我们可以用来方便的对类型进行描述。如下所示,dojo.declare的格式很简单:
- dojo.declare(className,superclass, props);
其中className定义了类型的名称。superclass指定了父类型,如果想声明的类并不存在父类,那么superclass可以为null。由于在Dojo中支持多继承,因此superclass可以是一个包含了所有父类的数组。最后的props是一个对象,其中包含了该类的所有字段以及方法。在props中可以定义一个特殊的函数constructor,constructor在该类型被实例化的时候会被自动调用到,相当于构造函数。例如:
- dojo.declare('People',null,{
- name:'unknown name',
- constructor:function(name){
- this.name=name;
- }
- });
- var p=new People('Jack');
(二) 定义继承
dojo. declare除了能够声明类,还能对类进行扩展,进而达到继承目的。这里我们只讨论单继承的例子,由于多继承较为复杂,第三章中会进行详细描述。在传统的面相对象语言(例如JAVA)中,子类能够添加父类中没有的字段以及方法,这也是继承最大的意义——扩展。扩展能补充父类不具备的功能,通过多层次的继承,逐渐构建出丰富而强大的模块。除了扩展,子类型需要拥有从父类型中继承字段、方法的能力,还需要屏蔽字段、覆写方法以及调用父类型方法(包括调用父类型的构造函数)的能力,而这些能力也是大型面向对象系统的基本特性。
但从原生态的JS来看,JS并不能完全满足这些要求。我们可以在一个JS对象中添加自己的属性,以此来屏蔽或者覆写原型对象中的属性,代价是这样做的话就失去了访问原型对象中的属性的能力。说白了,JS仅仅能够依靠回溯原型链来访问原型对象中的属性,这也是受原型继承机制的限制。
- dojo.declare('People',null,{
- name:'unknown name',
- action:function(){
- //do nothing
- },
- constructor:function(name){
- this.name=name;
- }
- });
- dojo.declare('Student',People,{
- school:'',
- action:function(){
- //I am studing
- },
- constructor:function(name,school){
- this.school=school;
- }
- });
- var s=new Student('Jack','Harvard');
- s.name // Jack
- s.school // Harvard
- s.action // I am studing
上面的代码主要完成了三件事情。第一,利用dojo.declare定义了一个类People。第二,利用dojo.declare定义了一个类Student。在定义Student的时候,第二个参数是People,即定义Student的父类为People。最后创建了一个Student的实例s,我们传进构造器的参数'Jack''Harvard'分别被赋给了s.name和s.school。虽然Student里的构造函数没有处理name属性,但是会自动调用父类People里的构造函数。从执行的结果来看,Student中添加了新的字段school,继承了People中的name,同时也对People中的action方法进行了覆写。现在已经完成了一个简单的单继承,下面我们来验证这个单继承结构的正确性。
- console.log(s instanceof Student); //true
- console.log(s instanceof People); //true
- console.log(s instanceof Object); //true
Dojo中的继承并不要求仅限于两个或者多个利用Dojo. declare创建出来的类。如果是利用其他的形式创建出来的类("raw" classes),Dojo中一样可以对它们进行扩展,定义他们的子类。例如dojo.declare可以指定父类为第一章中描述的’伪类’:
- //define superclass
- var Foo = function(){
- this.name = 'foo';
- };
- Foo.prototype.say = function(){
- console.log(this.name);
- };
- //define subclass
- var Bar = dojo.declare(Foo, {});
- var bar = new Bar();
- bar.say(); // foo
上例中Bar是Foo的子类,并且继承了Foo中的字段和方法。总的来说,dojo.declare是一种很自由的创建类的方式。
(三) 定义静态域
传统的面向对象语言都直接在语言层面上支持静态域的概念。例如JAVA,其静态域包括静态类型的字段以及方法,静态类型的字段由所有的实例共享,而静态方法可以由类型直接调用,故其中不能访问非静态字段(只能先产生实例,在通过实例访问字段)。JS并没有直接支持静态域这个概念,但可以通过模拟的方式来达到静态域的效果。下面一个例子展示了Dojo中如何定义静态域:
- dojo.declare("Foo", null, {
- staticFields: { num: 0 },
- add:function(){
- this.staticFields.num++;
- }
- });
- var f1=new Foo();
- var f2=new Foo();
- f1.add();
- f2.add();
- console.log(f1.staticFields.num ) //2
在这个例子中,我们定义了一个staticFields对象。该对象用来存放所有的静态字段以及静态方法。最终打印结果为2,也就是说staticFields中的变量确实由Foo的所有实例所共享。
事实上,staticFields并非Dojo中声明静态域的一个特殊标识符。能够被所有实例共享,与staticFields这个名称本身无关。如果我们用其他标识符来替代,比如 sharedVars,也会起到相同的静态域的效果。例如:
- dojo.declare("Foo", null, {
- sharedVars: [1,2,3,4]
- });
- var f1=new Foo();
- var f2=new Foo();
- f1.sharedVars.push(5);
- console.log(f2.sharedVars) //[1, 2, 3, 4, 5]
这个例子中的sharedVars是一个数组。当f1对这个数组进行修改以后,同样会反映到f2。总结一下可以得出,dojo.declare中直接声明的引用类型将会被所有实例共享。如果我们在使用dojo.declare的时候不注意这点,很有可能已经埋下了安全隐患。
- dojo.declare("Foo", null, {
- shared: [1,2,3,4],
- constructor:function(){
- this.unshared={ num:1 };
- }
- });
- var f1=new Foo();
- var f2=new Foo();
- f1.shared.push(5);
- f1.unshared.num++;
- console.log(f2.shared) //[1, 2, 3, 4, 5]
- console.log(f2.unshared) //Object { num=1 }
从这个例子可以看出来,放在constructor外面的引用类型会被实例共享,而放在constructor里面的则不会。如果不希望字段被共享,可以移至constructor函数中进行定义。在第一章中已经叙述,一个构造器的prototype对象中的属性将会被该构造器的所有实例共享。因此在使用构造器的情况下,我们可以往prototype中添加字段,来达到共享变量的目的。在dojo中其实也是这么做的。
上图揭示了dojo.declare实际处理的做法。在真正创建构造器Foo的过程中,除了constructor方法,其他所有声明的字段、方法都会被放进Foo.prototype中。而constructor方法会被当做Foo函数体来执行。由于this.unshared = { num:1 }是被放在Foo中执行的,所以每个实例都会拥有自己的unshared拷贝,另外shared在prototype中,因此被所有的实例共享。如果对dojo.declare细节感兴趣,可以参考第四章。
(四) 调用父类方法
在dojo中,一般情况下父类型的constructor(构造函数)总是自动被调用,第二小节的例子已经说明这一点。进一步说,父类型的constructor总是优先于子类型的constructor执行。让父类型的构造函数先于子类型的构造函数执行,采用的是after algorithm,还可以不自动调用父类构造函数,而采用手动调用,这些在第三章中会有具体描述。
- dojo.declare("Foo", null, {
- constructor:function(){ console.log('foo') }
- });
- dojo.declare("Bar", Foo, {
- constructor:function(){ console.log('bar') }
- });
- var b=new Bar; // 自动调用,打印foo bar
继承并不总是对父类中的方法进行覆写,很多时候我们需要用到父类中定义好的功能。因此dojo提供了调用父类中非constructor方法的能力,这也是对JS缺陷的一种弥补。具体的调用采用this.inherited方式。先看一个调用父类同名方法的例子:
- dojo.declare("Foo", null, {
- setPro:function(name){
- this.name=name;
- }
- });
- dojo.declare("Bar", Foo, {
- setPro:function(name,age){
- this.inherited(arguments); // 调用父类中的同名方法
- this.age=age;
- }
- });
- var bar=new Bar;
- bar.setPro('bar',25);
this.inherited可以在父类中寻找同名方法(这里是set),如果找到父类中的同名方法则执行,如果没有找到,继续在父类的父类中寻找。注意,this.inherited可以接受三种参数:
- methodName(可选)调用的方法名称,大多数时候这个参数是可以省略的。
- arguments(必选)通过传递arguments使得父类中的方法可以获取参数。
- argsArray(可选)一个数组,其中可以包含准备传递给父类方法的若干参数。如果存在该参数,那么父类方法中获取的将是这个参数,而不是arguments中的内容。
上面例子中的this.inherited(arguments) 可以改写成:
- ……
- this.inherited('setPro',arguments);
- ……
这种写法明确指定了准备调用的父类方法的名称。但是如果改写成:
- ……
- this.inherited('setPro',arguments,[ 'foo']);
- ……
- // bar = {name:'foo',age:25}
那么执行的结果是bar.name等于foo。这些都跟inherited的具体实现有关,深入到inherited函数,可以发现inherited大体上执行的是如下语句:
- function inherited(args, a, func){
- // crack arguments
- if(typeof args == "string"){
- name = args;
- args = a;
- a = func;
- }
- // find method f
- ……
- if(f){
- return a === true ? f : f.apply(this, a || args);
- }
- }
其中f就是父类中的方法,args是字面量arguments,a是另外传入的参数数组(argsArray)。如果额外传入的参数是true,那么直接返回f。当argsArray存在的情况下,将执行f.apply(this, a),否则执行f.apply(this, args)。除了this.inherited,还有一个类似的函数this.getInherited。这个函数仅仅获取指定的父类中的方法,但是并不执行。
- dojo.declare("Foo", null, {
- m1:function(){
- console.log('foo');
- }
- });
- dojo.declare("Bar", Foo, {
- m2:function(){
- // get method m1 from Foo
- var temp=this.getInherited('m1',arguments);
- temp.apply(this);
- }
- });
- var bar=new Bar;
如果对inherited或getInherited的实现细节感兴趣,可以参考第四章。
(五) 定义扩展(extend)
dojo.declare提供了一些函数,这些函数可以被很方便的调用,比如上面已经提到的isInstanceOf函数,inherited函数,getInherited函数。这些函数都是作用在某个类型的实例或者说对象上。此外,还提供了一个用于mixin的函数——extend()。与上面几个函数不同,extend直接被dojo.declare定义的类所调用。
extend最常见的用法是对类型进行扩展,增加原先没有的新属性(方法)。当然也可以用它来添加重名的属性,不过这样会有一定的风险替换掉原先已经定义的属性。
- dojo.declare('A',null,{
- func1:function(){ console.log('fun1')}
- });
- A.extend({
- func1:function(){ console.log('fun2')},
- func2:function(){ console.log('fun3')}
- });
- var a=new A;
- a.func1(); //fun2
- a.func2(); //fun3
拥有了extend能力的类型与传统静态语言中的类型很不一样。在一般的静态语言中,我们无法对一个现有的类型作出更改,这个时候如果需要对一个类型进行扩展,无非是选择继承、组合。而在Dojo中,即使已经完成了一个类型定义,如果将来有需要,我们依然可以随时使用extend对这个类型作出改变(替换或添加属性)。这种改变现有类型的能力,是JS带给我们的一种动态特性,Dojo只是对此作出了进一步封装。
- dojo.declare('A',null,{
- constructor:function(){
- console.log('1')
- }
- });
- A.extend({
- constructor:function(){
- console.log('2')
- }
- });
- var a=new A; //'1'
注意extend无法替换掉类定义中的构造函数。从本质上讲,extend与mixin没有任何区别。declare中的extend源码如下:
- function extend(source){
- safeMixin(this.prototype, source);
- return this;
- }
这里的this是指调用extend的类型。safeMixin会将source对象的属性、方法(构造函数除外)毫无保留的添加到this.prototype对象中。由于发生更改的是prototype的对象,因此extend之后,该类型的每个实例都会拥有新赋予的能力,不管该实例是在extend之前还是之后生成。
(一) 定义多继承
Dojo在基于类的面向对象系统方面增强了JS的表现力,在第二章中已经提到Dojo还允许用户使用多继承,本章将主要探讨关于多继承的内容。利用dojo.declare声明多继承的类是很方便的,用户只需要传递一个数组(superclass )进去,superclass数组包含了所有的父类。
- dojo.declare("A", null, {
- constructor: function() { console.log ("A"); }
- });
- dojo.declare("B", null, {
- constructor: function() { console.log("B"); },
- text: "text B"
- });
- dojo.declare("C", null, {
- getText: function(){ return "text C" }
- });
- dojo.declare("D",[A,B,C],{
- constructor: function() {
- console.log(this.text + " and " + this.getText());
- }
- });
- var d = new D();
- // A
- // B
- // text B and text C
该例声明了类型A、B、C、D,注意在声明类D的时候传入了superclass数组——[A, B, C],这使得D成为A、B、C的子类。运行上面代码会首先打印A,在执行A的构造器之后,其他基类的构造器也会按传入顺序被执行,D的构造器会被最终调用。同时,D也继承了A、B、C三个父类中其他域。
事实上,A是D的唯一一个真正的父类,这是由于Dojo在实现多继承的时候,仅仅将A采纳为D的父类,其他的‘父类’仅仅会被mixin进A(具体细节可以参考第三章第四小节)。但抛开实现的细节来看,用户真正需要得到的结果是D是A、B、C这三个类的子类。因此,这时候采用JS中的instanceof运算符并不能很好的判断类型。拿上例来说:
- d instanceof A //true
- d instanceof B //false
- d instanceof C //false
- d instanceof D //true
很显然,这不是用户想要的结果。为了应付在多继承环境下的类型判断,Dojo提供了类似的函数——isInstanceOf,方便用户进行类型判断。
- d.isInstanceOf(A) //true
- d.isInstanceOf(B) //true
- d.isInstanceOf(C) //true
- d.isInstanceOf(D) //true
(二) MRO与C3算法
面向对象语言如果支持了多继承的话,都会遇到著名的菱形问题(Diamond Problem)。假设存在一个如左图所示的继承关系,O中有一个方法foo,被A类和B类覆写,但是没有被C类覆写。那么C在调用foo方法的时候,究竟是调用A中的foo,还是调用B中的foo?
不同语言对这个问题的处理方式有所不同。例如C++中采用虚继承,而Python中采用MRO的方式来解决。MRO又称作方法解析顺序(Method Resolution Order),即查找被调用的方法所在类时的搜索顺序。在Python中一共出现过三种MRO,Dojo中采纳了Python2.3版本之后的MRO算法,该算法简称C3算法。
C3算法简单来说是将一个类型以及它的所有父类进行线性化排列。之所以进行线性排列,其实是想让这些类按照某种重要程度排序,然后实际调用方法的时候,在这个线性序列中从前向后依次寻找,最靠前的方法才会被调用到。比如上面图片中的这个例子,在Python中可以描述为:
- >>> O = object
- >>> class A(O): pass
- >>> class B(O): pass
- >>> class C(A,B): pass
对C进行C3算法,得到的结果表示为L(C)=CABO.这个结果看起来很像是广度优先搜索的结果,事实上它们之间是有点类似,但不总是相同。得到的线性化序列CABO保证了Python在调用方法的时候,C是第一个被搜索的,A总是优先于B被先搜索到。
C(B1 ... BN) :C类的父类是B1…BN类
L(C(A,B)) :C以及C的所有父类的线性化排列
merge(A,B) :A和B的合并
CABO的头部:C
CABO的尾部:ABO
C3算法:
如果一个类没有父类,那么该类的线性化为:
L(C)=C
如果一个类有一个或多个父类,那么该类的线性化为:
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
merge的方法:
1. 取出merge中参数列表的第一个元素,取第一个元素的头部,如果该头部不在其他元素的尾部,则该头部合格,在merge列表中排除它,并把它当做结果的头部。
2. 如果该元素的头部在其他元素的尾部,则跳到该元素的下一个元素。取出该元素的头部,判断它是否合格。合格则在merge列表排除它并放入结果中,不合格则重复该步骤。
3. 最终直到所有的类都被删除,则merge成功,或者无法找到合格的头部,如果无法找到合格的头部,则merge失败并报出异常。
上面细述了C3算法,注意我们在定义一个类的时候,传入这个类的父类的顺序直接决定了最后线性化结果的顺序。下面来看一个复杂一些的例子。
- >>> O = object
- >>> class F(O): pass
- >>> class E(O): pass
- >>> class D(O): pass
- >>> class C(D,F): pass
- >>> class B(E,D): pass
- >>> class A(B,C): pass
这里有四层继承结构。我们从上到下逐层计算线性化序列:
L[E] = E+merge(L[O],O)
= E+ merge(O,O)
= EO
L[D] = DO
L[F] = FO
L[B] = B+merge(L(E),L(D),ED)
= B+merge(EO,DO,ED)
= BE+merge(O,DO,D)
= BED+merge(O,O)
= BEDO
L[C] = CDFO
L[A] = A+merge(L(B),L(C),BC)
= A+merge(BEDO,CDFO,BC)
= AB+merge(EDO,CDFO,C)
= ABE+merge(DO,CDFO,C)
= ABEC+merge(DO,DFO)
= ABECD+merge(O,FO)
= ABECDF+merge(O,O)
= ABECDFO
从L(A)=ABECDFO来看,最终A类对象调用方法时是按照ABECDFO的优先顺序来搜索的。利用C3算法计算的时候需要注意并不是所有的继承结构最后都能导出线性化的序列。C3算法的第三步骤允许我们失败。假设有下面这样的继承结构:
- >>> O = object
- >>> class A(O): pass
- >>> class B(O): pass
- >>> class C(A,B): pass
- >>> class D(B,A): pass
- >>> class E(C,D): pass
对该继承结构计算线性化序列:
- L[O] = O
- L[A] = AO
- L[B] = BO
- L[C] = CABO
- L[D] = DBAO
- L[E] = E+merge(L(C),L(D),CD)
- = E+merge(CABO,DBAO,CD)
- = EC+merge(ABO, DBAO,D)
- = ECD+merge(ABO, BAO)
当进行到L[E] = ECD+merge(ABO, BAO)这一步时已经无法再进行下一步merge计算。所以对E利用C3算法失败。得到失败的结果也是合情合理的,从直观上讲,如果E的对象调用从A或B中继承来的方法,无法判断究竟该调用A中的还是B中的。由于是利用C(A,B)和D(B,A)这样来构建,所以没法得知A和B谁对E来说更加“重要” 。
(三) Dojo中的MRO
dojo中MRO的处理方式与Python有一点点小区别。Python在构建对象的时候传入父类列表,越靠前的类越容易被搜索到,代表着对新建的类越重要。反之,如果一个父类处在越高的继承层次上,则越不容易被优先搜索到。dojo中的MRO大体上可以参考上节中的描述。但是略有区别,描述如下:
如果一个类没有父类,那么该类的线性化为:
L(C)=C
如果一个类有一个或多个父类,那么该类的线性化为:
L[C(B1 ... BN)] = C + merge(L[BN] ... L[B1])
//python中:
// L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
具体的区别已经在上面的算法描述中被标识出,可以看出,merge的参数不大一样,少了B1 ... BN序列,而且传入参数的顺序发生了变动。不过具体的merge做法与Python中一样。正是因为传入的参数顺序与Python中完全相反,造成了Dojo中有一种越是靠后的类越是被优先搜索到的趋势。
下面举例来具体说明Dojo与Python中MRO的区别。假设有如左图所示的继承,分别计算MRO顺序:
通过上述的例子可以发现,由于merge中传入参数的顺序不同,导致最终得出的MRO顺序不同。整体上Python倾向于一种类似广度优先搜索的顺序,而Dojo中的结果呈现出一种深度优先的搜索顺序,不过实际上并不是很准确。
除了在整体上反映出不同的优先顺序,Dojo中的MRO做法实际上避免了许多MRO失败。在上一小节已经描述过一种情况,由于父类均是从同样的类型继承而来,但是继承的顺序不同,导致子类无法确定优先级关系,因此merge步骤失败。还有一种情况是,如果父类之间彼此也存在继承关系,那么同样会容易导致MRO失败,比如说下面所示的继承。
如上图所示,C类的两个父类A和B之间发生了继承关系。在Python的MRO中,右边的一个继承关系是失败的。利用C3算法可以很快的推导出来。
L[B] = BA
L[C] = C+merge(L(A),L(B),AB)
= C+merge(A,BA, AB ) // Python中的MRO失败了
类似,左边的继承关系在Dojo中也应该是失败的。因为Dojo和Python中继承结构的线性化大体上是左右相反的。但实际上,无论是左边还是右边的继承关系,在Dojo中都是成功的。在Dojo中,分别针对左边和右边的继承进行MRO计算:
在Dojo中左边的继承能够MRO成功,主要原因是merge时传入的参数比Python中少了父类型序列。如下所示:
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN ) //Python中还需传入父类型的序列
如果B1 ... BN之间(即父类型之间)彼此不存在继承关系,那么是否传入父类型序列对merge的结果是不造成影响的。但是如果B1 ... BN之间存在了继承关系,那么merge的时候,B1 ... BN将会结果造成直接影响。不传入父类型的序列,这正是Dojo中能够成功避免一些MRO失败的原因,也可以说,Dojo中的MRO并不像Python中那么严格。
Dojo中的MRO计算是通过c3mro函数来进行的,传入的参数是dojo.declare声明时的superclass数组。如果想知道c3mro实现的细节,可以参考第四章。
(四) mixin与多继承
JS中的原型继承方式并不能支持多继承,因为每个构造器仅仅能指定一个原型对象,这是一种单继承形式,所以在Dojo中也仅仅是尽量去模拟多继承,而并非真正的多继承。故本章标题中采用的多继承字样是不准确的,准确的说,在Dojo中使用的是mixin与单继承结合的方式。只有一个类充当真正的父类,其余的类会被用于mixin。
mixin是指将属性添加到指定的对象中,这是一种很常用的扩展对象的手段。mixin行为发生在两个对象之间,源对象(source)和目标对象(target)。大体来说,mixin会遍历source中的属性,并且添加到target中去。,如果在target中已经存在了同名的属性,那么需要在mixin中进一步判断,是否需要将这些同名属性覆盖。一个简单的mixin实现如下:
- function mixin(target, source){
- for(name in source){
- target[name] = source[name];
- }
- }
实际上Dojo中mixin的也类似于这样来实现,只是加了一些判断条件。
在上一节中已经描述过Dojo中的MRO计算。在Dojo.declare进行处理的时候,首先对superclass进行MRO计算,并返回一个由构造器组成的数组。紧接着需要根据这个数组(序列),构建出原型链。该原型链中包含了所有数组中出现的构造器,包括在superclass中的和不在superclass中的。只有当这条原型链被构建好,关于继承所做的工作才真正完成。在构建原型链的过程中,Dojo不断的利用mixin与匿名函数的组合,模拟出多继承的实现。举例来说:
- dojo.declare('A',null,{
- funA:function(){}
- });
- dojo.declare('B',null,{
- funB:function(){}
- });
- dojo.declare('C',null,{
- funC:function(){}
- });
- dojo.declare("D",[A,B,C],{});
- new D();
对于上述例子中的D类,传入的superclass为[A,B,C],计算出的MRO序列为:[C,B,A]。构造器A作为整个继承结构的最顶端,可以看做是D类的真正父类。至于B类、C类,都在构造原型链的过程中,被mixin进了某个匿名对象中。下面是构建后的继承图:
利用dojo.declare声明的时候,只有一个类被当作父类,其余所有传入的类仅仅做mixin用。通常是superclass中的第一个类会被当做父类,即对于继承C(B1 ... BN),B1会被当做C的父类,不过这是有前提的,即L(C)的末尾完全由L(B1)构成。大部分情况下,这个前提都是可以满足的,但是也有不满足的情况,这时候所选取的父类就是L(C)中的最后一个类。举例来说:
可以用JS提供的instanceof来判断是否是父类型的实例。而针对其他mixin的类型使用,则会失败,这时候可以用第二章中描述过的isInstanceOf函数。例如,对于上面的例一:
- var f = new F();
- console.log(f instanceof A ); //false
- console.log(f.isInstanceOf(A)); //true
- console.log(f instanceof B ); //false
- console.log(f instanceof C ); //false
- console.log(f instanceof E ); //false
- console.log(f instanceof F ); //true
- console.log(f instanceof D ); //true
根据L(F)= FECBAD可以看出,类型F处于继承结构的最底端,而类型D是F的父类,处于最顶端。这两个类型都能够直接被instanceof识别,其余的ABCE都只能利用Dojo提供的isInstanceOf才能返回true。
这是dojo.declare中的三个极度蛋疼的功能,在对多继承的实质有所了解之后,才会加深对这三个功能的认识,所以放到最后说。这里就不谈它们的实现原理了,第四章中也许会描述到= =!
如果觉得运行constructor前后缺少了些什么,那么preamble、postscript可以很好的帮助我们进行弥补。根据我时间不长的开发经验,还想不出什么情况下需要这种操作来弥补。如果在类型的定义中包含了preamble方法,那么在这个类型的构造函数被调用之前,会首先执行一次preamble。同样如果定义了postscript方法,那么该类型的构造函数被调用之后,也会自动执行一遍postscript。下面是一个简单的例子:
- dojo.declare('A',null,{
- preamble:function(){ console.log('A'); },
- constructor:function(){ console.log('AA'); },
- postscript:function(){ console.log('AAA'); }
- });
- var a= new A();
- /*输出:
- A
- AA
- AAA
- */
至于preamble和postscript方法究竟是如何被调用的,第四章中有解释,暂时不需要关注,可以认为这是Dojo提供好的机制。来个复杂一些的例子:
- dojo.declare('A',null,{
- preamble:function(){ console.log('A'); },
- constructor:function(){ console.log('AA'); },
- postscript:function(){ console.log('AAA'); }
- });
- dojo.declare('B',A,{
- preamble:function(){ console.log('B'); },
- constructor:function(){ console.log('BB'); },
- postscript:function(){ console.log('BBB'); }
- });
- dojo.declare('C',B,{
- preamble:function(){ console.log('C'); },
- constructor:function(){ console.log('CC'); },
- postscript:function(){ console.log('CCC'); }
- });
- var c= new C();
- /* 输出:
- C
- B
- A
- AA
- BB
- CC
- CCC
- */
从输出的结果来看,我们可以挖掘出一些有意思的事情。在这种拥有继承的情况下,父类中postscript方法是不会被自动调用到的。上述例子的准确函数执行顺序是:
2. B.preamble
3. A.preamble
4. A.constructor
5. B.constructor
6. C.constructor
7. C.postscript
至于为什么不会调用到A和B的postscript方法,从Dojo的源码实现上讲是因为这里所调用的父类型的constructor并没有去执行postscript方法。换个角度说,这里调用父类型的constructor函数完成的构造过程,与我们直接通过new来调用的父类型发生的构造,是两回事。归纳来说,对类型L(A)= AA1A2A3…AN使用new进行实例化时,默认的执行顺序是:
AN.constructor-> AN-1.constructor ... A.constructor->
A.postscript
在Dojo1.4之后的版本中,preamble已经被标记为deprecated函数,不过postscript并没有被列入deprecated。chain提供了自动执行父类中函数的功能。默认情况下,只有父类的构造函数是会被自动调用的,并且总是先于子类的构造函数执行。只有在一些特殊情况下,我们会需要让其他的函数也能够像构造函数一样,自动执行,免去我们手工调用的麻烦。举例来说,如果创建的类型包含了destroy函数,该函数会进行一些垃圾回收方面的工作,我们肯定希望destroy函数完成后也会自动去执行一下父类中的destroy。
下面的例子定义了一条destroy函数组成的chain。其中的允许我们来设置函数的执行顺序,这里指定的是before顺序,也就是说子类的函数会先于父类的函数执行,所以子类的destroy先运行。
- dojo.declare('A',null,{
- constructor:function(){ console.log('A'); },
- destroy:function(){console.log('AA');}
- });
- dojo.declare('B',A,{
- constructor:function(){ console.log('B'); },
- destroy:function(){console.log('BB');}
- });
- dojo.declare('C',B,{
- "-chains-": {
- destroy: "before"
- },
- constructor:function(){ console.log('C'); },
- destroy:function(){console.log('CC');}
- });
- var c= new C();
- c.destroy();
- /*输出:
- A
- B
- C
- CC
- BB
- AA
- */
有两点值得注意:
第一点是"-chains-"语句所处的位置,上例中放在了C类型的定义中。如果放在A或者B类中,执行c.destroy()的效果还是一样的。事实上,只要把chain声明放在继承链条中的任何一个类型定义里,都可以达到串连所有同名函数的效果。对于复杂的多重继承结构也是这样的,因为他们实质上最终还是一条单继承结构。
第二点是chain中允许我们声明三种类型的顺序,他们能够产生效果的对象不同。字面上,我们能够使用的是aftereforemanual这三个顺序,他们分别代表了在父类函数执行之后执行、在父类函数执行之前执行、手动调用。对于非构造函数,设置manual是没有意义的,如果不是after顺序,会被一概视为before。而对于构造函数,设置before是没有意义的,因为父类的构造函数要么manual手动调用,要么一定会在子类的构造函数之前执行。
- dojo.declare('A',null,{
- "-chains-": {
- constructor: "before", //没有作用,非‘manual’即被视为‘after’
- foo: "manual" //没有作用,非‘after’即被视为‘before’
- },
- constructor:function(){ console.log('A'); },
- foo:function(){console.log('AA');}
- });
- dojo.declare('B',A,{
- constructor:function(){ console.log('B'); },
- foo:function(){console.log('BB');}
- });
- dojo.declare('C',B,{
- constructor:function(){ console.log('C'); },
- foo:function(){console.log('CC');}
- });
- var c= new C();
- c.destroy();
- /*输出:
- A
- B
- C
- CC
- BB
- AA
- */
最后来看一个针对构造函数设置manual的例子。
- dojo.declare('A',null,{
- "-chains-": { constructor: "manual" },
- constructor:function(){ console.log('A'); }
- });
- dojo.declare('B',A,{
- constructor:function(){ console.log('B'); }
- });
- dojo.declare('C',B,{
- constructor:function(){
- this.inherited(arguments); //设置为manual后,只能手动调用父类函数
- console.log('C');
- }
- });
- var c= new C();
- /*输出:
- B
- C
- */
从这个例子可以看出,在设置了manual后,如果不手动去调用父类的构造函数,那么父类的构造函数是不会执行的,因此这里就不会打印A,根据第二章中的描述,手动调用可以使用inherited方法。
PS,之前我以为preamble、postscript以及chain会在dijit中被较多使用到,但根据在Dojo1.5源码中的搜索,很不幸,只有postscript在dijit中被使用过,至于preamble和chain基本上在整个Dojo的实现代码中都没有,只有在test的代码里出现过两三次。可见这些功能偏门 到什么程度。我觉得API提供的原则应该是简单易用 ,而Dojo的接口往往体现着庞大复杂精深,我想这可能也是很多web fronter不愿意花成本去学习去使用的Dojo的原因吧。其实作为开发者来说,Dojo用熟了也并没有感觉太复杂,你甚至会为它的细致周全感到震撼,但是对于初学者来说,或者是那些急于上手某个Ajax框架进行开发的人,Dojo的确不是一个好的选择。
declare.js中包含了整个dojo面向对象中最重要的代码,即对类型表达和扩展的一些封装。功能虽然强大,但是幸好文件并不复杂,拥有清晰的脉络。整个declare.js一共定义了15个函数,14个具名函数,1个匿名函数。这14个具名函数中又有一些是declare.js内部使用的函数,外部无法调用,还有一些是由dojo提供的接口,可以供dojo.declare声明的类型来调用。具体函数如下所示:
- //declare.js的结构(来源于Dojo1.5版):
- (function(){
- //9个内部函数,外部无法调用
- function err(){...}
- function c3mro(){...}
- function mixOwn(){...}
- function chainedConstructor(){...}
- function singleConstructor(){...}
- function simpleConstructor(){...}
- function chain(){...}
- function forceNew(){...}
- function applyNew(){...}
- //5个对外提供的接口
- function inherited(){...}
- function getInherited(){...}
- function isInstanceOf(){...}
- function extend() {...}
- function safeMixin(){...}
- //1个匿名函数:Dojo.declare
- d.declare = function(){...}
- })();
其中最为核心的即匿名函数,用户在使用dojo.declare的时候即是使用的该函数。该函数负责产生一个实实在在的可以new出实例的JS构造器。本章打算从该函数开始,逐渐分析整个declare.js文件。
(一) dojo.declare
1) crack parameters
先来看declare的参数列表以及函数的开头部分。其中的d在整个declare.js的开始即被赋值为dojo,至于传入的三个参数className,superclass,props曾经在第二章中有过解释。其中className代表要声明的类型的名称,superclass指定了父类型列表,最后的props对象包含了类型的所有构造函数、字段、方法。
- d.declare = function(className, superclass, props){
- // crack parameters
- if(typeof className != "string"){
- props = superclass;
- superclass = className;
- className = "";
- }
- props = props || {};
- ……
- }
由于dojo.declare支持声明一个匿名类型,因此参数列表中的第一个参数className可以省略,所以才有了crack parameters 这一步骤。其实最后一个props参数也可以省略,如果这样dojo会自动用一个空的对象代替。
- //正常的类型声明
- dojo.declare('A',null,null);
- //匿名类,继承自类型A
- var B = dojo.declare(A);
2) 计算MRO、指定父类型
在处理完传入的参数之后,紧接着的一步就是计算MRO继承序列。当然,前提是用户在使用dojo.declare声明的时候传入了superclass。之前一直superclass说是一个包含了多个类型的数组,其实superclass也可以不是数组类型,在单继承的时候,superclass就是唯一的一个父类型,另外在没有继承的时候,superclass一定要写成null。
所以在代码的实现中,首先判断了superclass是不是一个数组。这里判断数组的时候利用了 Object.prototype.toString函数(这里的opts),去年我在求职的时候不少公司都问到了这个问题:JS中如何检测数组类型,可惜当时没有好好研究过,所以都不能算答上。
如果这里确定了superclass是一个数组,那么则调用c3mro函数来计算MRO。注意这里返回的MRO数组中第一个保存的并非某类型本身,而是一个偏移量,表示实际上的唯一一个父类在MRO结果中距离顶端的偏移量。有了这个偏移量,自然很方便的可以从MRO结果中获取父类型。
如果superclass不是一个数组,而是一个构造器,那么肯定这是一个单继承的情况。这里的判断也很有意思,同样是利用的Object.prototype.toString函数,如果返回的结果为object Function,那么superclass肯定是一个函数(构造器)。接下来同样是构造存放MRO的bases数组。
- // 如果传入的superclass是一个数组,那么调用c3mro来计算MRO
- // 计算出来的结果放在bases数组中
- if(opts.call(superclass) == "[object Array]"){
- // bases中保存了根据superclass计算出来的MRO
- bases = c3mro(superclass);
- // 其中bases[0]保存了真正的那个父类在MRO序列中距离顶端的距离
- t = bases[0];
- mixins = bases.length - t;
- superclass = bases[mixins];
- }
- // 如果传入的superclass是不是数组,那么判断它是构造器还是null
- // 既不是构造器也不是null则会抛出异常
- else{
- bases = [0];
- if(superclass){
- // 如果传入的superclass是一个构造器
- if(opts.call(superclass) == "[object Function]"){
- // 判断superclass是raw class还是Dojo中的类
- t = superclass._meta;
- // 加入superclass的bases
- bases = bases.concat(t ? t.bases : superclass);
- }else{
- err("base class is not a callable constructor.");
- }
- }else if(superclass !== null){
- err("unknown base class. Did you use dojo.require to pull it in?")
- }
- }
这里还有一个值得注意的地方是,superclass既可能是一个原生态的JS构造器,也可能是利用dojo.declare声明出来的构造器。如果是Dojo中的构造器,那么可以直接获取superclass._meta.bases,这里记载了superclass的MRO序列。如果仅仅是JS原生的构造器,那么会将superclass本身加入bases。
3) 构造单继承链
有了MRO序列之后就应该构造一条单继承结构,整个过程大概类似于从MRO中的父类开始,逐渐向前取出MRO中的类型,mixin进一条继承链。这个过程之前已经用过一个实例来表示出来,如第三章第四小节中的图所示。
- // 构造完整的继承链
- if(superclass){
- // 从bases中的父类型开始向前扫描,直到i=0时为止
- for(i = mixins - 1;; --i){
- // superclass是目前已经处理到的类型
- // proto是从superclass.prototype继承来的一个对象
- proto = forceNew(superclass);
- if(!i){
- break;
- }
- // t是bases中位于superclass前的类型
- t = bases[i];
- // 在proto中mixin进t.prototype中的属性
- (t._meta ? mixOwn : mix)(proto, t.prototype);
- // 创建新的superclass,其prototype被设为proto
- // 等于该superclass类型继承自原先的superclass
- ctor = new Function;
- ctor.superclass = superclass;
- ctor.prototype = proto;
- superclass = proto.constructor = ctor;
- }
- }
- else{
- proto = {};
- }
- // 这里的props是dojo.declare时传入的参数
- // safeMixin会将props中除了constructor的属性都mixin进proto中
- // 由于proto最后充当了ctor.prototype,因此这些属性都会被当做static属性
- // 被所有ctor的实例共享,ctor即为dojo.declare最终产生的类型。
- safeMixin(proto, props);
- // 如果在props中有自定义的constructor,则赋给proto.constructor
- t = props.constructor;
- if(t !== op.constructor){
- t.nom = cname;
- proto.constructor = t;
- }
这里代码中的for循环会从父类型的前一个元素开始依次遍历,mixins代表了父类型在bases(MRO的计算结果)中的位置,整个循环的结束会在i=0时结束。3.4章节的这个例子中,父类型位于bases的最末尾,在继承链中也是出于最高点,所以for循环也完整的遍历了一次bases数组。但是并非所有的继承结构都会导致完整的遍历bases数组,既然是在bases中从父类型开始向前遍历,那么只要父类型如果不处于bases的末尾,就无需整个遍历bases了。举例来说:
- dojo.declare('A',null,{});
- dojo.declare('B',A,{});
- dojo.declare('C',B,{});
- dojo.declare("D",[C,B,A],{});
对于这样一个继承结构,理论上计算出来的D类的MRO为L(D)= DCBA。实际上通过c3mro方法返回的bases数组是[3,C,B,A],那么指定出的父类型就是C(右起第3个)。当superclass为C的时候,mixins=1,i=0,这里的for循环就不会再处理到B和A,这时候仅仅执行了一条proto = forceNew(superclass)语句就跳出了循环。这样的处理也是有道理的,因为C其实已经继承自B和A,也就是说如D仅仅需要继承了C,就会自动继承B和A中的属性,所以再处理B和A是没有意义的。
4) 处理chains
这一部分的内容相对简单,代码也很容易理解,但有一些逻辑上的细节需要注意一下。
- // 依然是从bases中的superclass前一个位置开始
- // 依次将各个类型中的chains定义加入到chains中
- for(i = mixins - 1; i; --i){
- t = bases[i]._meta;
- if(t && t.chains){
- chains = mix(chains || {}, t.chains);
- }
- }
- // 将proto中定义的chains加入chains
- if(proto["-chains-"]){
- chains = mix(chains || {}, proto["-chains-"]);
- }
这里chains是declare.js开头就定义好的一个对象,专门用来存储一个类型中的chains。代码中首先会从i = mixins - 1开始遍历,逐步将bases中类的chains加入到这里的chains来。在for循环结束之后,会最后再加入proto里的chains。
代码中似乎是少了对superclass类型的处理,是的。。。。。的确没处理。。。。。这会造成一个诡异的问题,依然是前面出现过的继承结构:
- dojo.declare('A',null,{
- "-chains-": {bar: "after"},
- foo:function(){console.log('A')},
- bar:function(){console.log('A')}
- });
- dojo.declare('B',null,{});
- dojo.declare('C',null,{});
- dojo.declare("D",[A,B,C],{
- "-chains-": {foo: "before"},
- foo:function(){console.log('D')},
- bar:function(){console.log('D')}
- });
- var d = new D();
- d.foo();
- d.bar();
根据上面代码的意思,我们预期在执行bar函数的时候,会输出A、D。但是实际上,在执行bar的时候,仅仅会输出一个D。这是因为A中定义的chains失效了,理由其实很简单,因为在处理chains的时候for循环中并不处理A,因为A是D的真正父类。然后再处理proto["-chains-"]的时候,又没有处理到A,因为D自带了一个proto["-chains-"],覆盖掉了A中的chains。这就造成了A中定义chains失效了的局面。因此最正确的定义chains的方式是只在一个类型中定义,比如上例统一在D中进行定义,只有这样才可以避免上述情况。
5) 创建ctor
这边的ctor就是最后dojo.declare定义出来的类型,其本质上是JS中的一个构造器。
- t = !chains || !chains.hasOwnProperty(cname);
- bases[0] = ctor =
- (chains&&chains.constructor==="manual") ? simpleConstructor(bases) :
- (
- bases.length == 1 ? singleConstructor(props.constructor, t) :chainedConstructor(bases, t)
- );
这里的t是一个flag,如果在之前的chains中已经包含了constructor 的设置,则为false,别的情况下都是true。下面则是分情况调用三个不同的函数来构造ctor:
- 如果chains中设置了constructor==="manual",则调用simpleConstructor
- 如果bases.length == 1即没有父类,谈不上继承,调用singleConstructor
- 最后如果不是上面两种情况,调用chainedConstructor
如果没有chains,这边的代码将美妙许多,既不需要为了构造chains去遍历bases,也不用调用chainedConstructor这样令人费解的函数。这边暂且不管上述三个铸成ctor的函数。接下来的代码就已经很容易理解了,虽然不短,但是都是常规的一些做法。具体解释如下:
- // 添加meta信息
- ctor._meta = {
- bases: bases,
- hidden: props,
- chains: chains,
- parents: parents,
- ctor: props.constructor// 这是用户定义时写的constructor函数
- };
- // 这边的superclass是最后一个被mixin进继承链的类型
- ctor.superclass = superclass && superclass.prototype;
- // 这边的superclass是最后一
- ctor.extend = extend;
- // 将ctor的原型设置成proto
- ctor.prototype = proto;
- proto.constructor = ctor;
- // 在proto中添加三个函数,用于new出来的对象调用
- proto.getInherited = getInherited;
- proto.inherited = inherited;
- proto.isInstanceOf = isInstanceOf;
- // 如果有className,那么就不是一个匿名类型,需要调用到Dojo.setObject向
- // dojo.global来注册,在浏览器中就是把ctor添加在window对象中。
- if(className){
- proto.declaredClass = className;
- d.setObject(className, ctor);
- }
meta信息可以用来判断一个类型是由dojo.declare生成的还是raw class。如果是raw class,那么肯定不会包含这些meta信息。这里对外提供了四个接口函数可以用供调用,一个是extend,还有三个是getInherited、inherited、isInstanceOf,之前都已经提过它们的用法,这里不再赘述。
6) 处理chains并返回ctor
如果没有chains这样的功能,那么刚开始的if语句中的代码也是可以省略的。如果chains中已经设置了一些函数的调用顺序,那么这里是需要进一步作出处理的。具体的处理方法是将原来的函数替换成执行chain后返回的同名函数,从实现可以看出来为什么对于非after的方法即看作为before。
- // 针对chains中除了constructor意外的函数作出处理
- if(chains){
- for(name in chains){
- if(proto[name] && typeof chains[name] == "string" && name != cname){
- t = proto[name] = chain(name, bases, chains[name] === "after");
- t.nom = name;
- }
- }
- }
- return ctor;
最后需要将ctor返回出去,因为dojo.declare允许进行匿名类型的定义,这时候不会再向dojo.global注册。
(二) getInherited和inherited方法
这两个方法是对外提供的扩展接口,可以用declare出来的类型的实例调用。它们的用途是返回父类中曾经出现的方法,或者对其进行调用。具体的用法在第二章中已经描述,这里不再举例说明。
1. getInherited方法主要用来返回父类中的方法;
2. inherited方法主要用来调用父类中的方法;
首先来看getInherited方法,该方法的实现比较简单:
- //getInherited的实现,调用了inherited
- function getInherited(name, args){
- //如果传入name,则返回指定name的方法
- if(typeof name == "string"){
- return this.inherited(name, args, true);
- }
- //否则返回的是父类中的同名方法
- return this.inherited(name, true);
- }
上面的实现中可以看出,在getInherited中其实还是调用inherited来做进一步处理,只不过将inherited的最后一个参数设置为true,在inherited里会判断该参数,为true就是返回函数,不为true则是调用函数。
接下来看inherited方法,可以分成个3步骤依次来看inherited具体的实现细节。第一步 依然是crack arguments,这是由于dojo提供的API的灵活性所致,就要求这样也能调用,那样也能调用,很蛋疼。第二步 就是根据name参数来寻找父类中的同名方法,这里的name有可能是constructor,也就是说我们可以利用inherited去调用父类中的构造函数,因此在inherited的实现里做了分情况讨论,一种情况是调用父类的非构造函数,还有一种是调用构造函数。想想在第三章末尾曾讲述过,可以把某继承链的构造器调用顺序设置为manual,这样将会破坏默认的自动调用父类构造函数,用户可以根据自己手动去调用父类的构造函数,用的正是this.inherited('constructor',arguments)....在第二步完成之后,第三步 所做的工作很简单,即决定是返回找到的同名方法还是调用这个同名方法。
- function inherited(args, a, f){
- var name, chains, bases, caller, meta, base, proto, opf, pos,
- cache = this._inherited = this._inherited || {};
- // 首先是crack arguments
- // 最后传入的参数f可能是true,也可能是一个替代args的数组,还有可能是默认的undefined
- if(typeof args == "string"){
- name = args;
- args = a;
- a = f;
- }
- f = 0;
- //args是子类方法的参数列表,args.callee代表在子类的哪个方法中调用了inherited
- caller = args.callee;
- //获取欲调用的父类的方法的名称,如果没有传入name参数,那么就是调用父类中的同名方法
- name = name || caller.nom;
- if(!name){
- err("can't deduce a name to call inherited()");
- }
- //这里获取到的是子类型的meta信息,下面接着通过meta信息来进一步获取子类型的MRO链
- meta = this.constructor._meta;
- bases = meta.bases;
- //第一次调用inherited的时候,由于缺少this._inherited信息,
- //所以cache是一个空的Object,这里pos是undefined
- //但是如果第二回及以后用到了inherited
- //那么在cache中记录了之前一次利用inherited寻找的方法和位置
- //注意实际上整个实现中并未利用cache,这里的cache疑似某个实现版本遗留下的痕迹
- pos = cache.p;
- //分情况讨论
- //1.要调用的方法不是父类中的构造函数
- if(name != cname){
- // method
- if(cache.c !== caller){
- // cache bust
- pos = 0;
- base = bases[0];
- meta = base._meta;
- if(meta.hidden[name] !== caller){
- // error detection
- chains = meta.chains;
- if(chains && typeof chains[name] == "string"){
- err("calling chained method with inherited: " + name);
- }
- // find caller
- do{
- meta = base._meta;
- proto = base.prototype;
- if(meta && (proto[name] === caller && proto.hasOwnProperty(name) || meta.hidden[name] === caller)){
- break;
- }
- }while(base = bases[++pos]); // intentional assignment
- pos = base ? pos : -1;
- }
- }
- // 在正常情况下,在bases[0]中根据name寻找到的方法就是caller
- // 因此需要沿着bases继续寻找,有可能会进入while循环,找到的函数放在f中
- base = bases[++pos];
- if(base){
- proto = base.prototype;
- if(base._meta && proto.hasOwnProperty(name)){
- f = proto[name];
- }else{
- opf = op[name];
- do{
- proto = base.prototype;
- f = proto[name];
- if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){
- break;
- }
- }while(base = bases[++pos]); // intentional assignment
- }
- }
- //这个写法太高级了....就是在bases中没有去就Object.prototype里找
- //不过很可能Object.prototype中依然没有名为name的方法,这时候f就是undefined
- f = base && f || op[name];
- }
- //2.要调用的方法是父类中的构造函数
- else{
- if(cache.c !== caller){
- //如果name是constructor,依然是沿着bases依次寻找
- pos = 0;
- meta = bases[0]._meta;
- if(meta && meta.ctor !== caller){
- // error detection
- chains = meta.chains;
- if(!chains || chains.constructor !== "manual"){
- err("calling chained constructor with inherited");
- }
- // find caller
- while(base = bases[++pos]){ // intentional assignment
- meta = base._meta;
- if(meta && meta.ctor === caller){
- break;
- }
- }
- pos = base ? pos : -1;
- }
- }
- // 这里找到的父类型的构造函数是base._meta.ctor
- // 即当初declare该类型时props中自定义的constructor函数
- while(base = bases[++pos]){ // intentional assignment
- meta = base._meta;
- f = meta ? meta.ctor : base;
- if(f){
- break;
- }
- }
- f = base && f;
- }
- // 把f和pos放进cache中,即this._inherited
- // 这样下次再次利用inherited寻找name方法的时候就很方便了
- cache.c = f;
- cache.p = pos;
- // 决定是返回f还是调用f
- if(f){
- return a === true ? f : f.apply(this, a || args);
- }
- }
上面不仅贴出了整个inherited的实现,也标注了一些关键步骤的解释。其中的cache疑似是一个过期的实现,因为实在想不出会有什么情况下cache.c === caller,除非人为的故意设置成如此,不过幸好即使忽略掉cache的作用也不会影响整段代码的理解。还有一个颇为有趣的地方在于如果发现了一个方法处于chains之中,那么会抛出异常,因为对一个已经chains的方法再去手动调用是毫无意义的。
(三) isInstanceOf方法
在第二章中已经提及,为了弥补JS自带的instanceof运算符无法判断Dojo中的继承,所以才有了isInstanceOf扩展。该方法由Dojo中类型的实例来调用。
- function isInstanceOf(cls){
- //获取该类型meta信息中的bases(即MRO的结果)
- var bases = this.constructor._meta.bases;
- //遍历bases
- for(var i = 0, l = bases.length; i < l; ++i){
- if(bases[i] === cls){
- return true;
- }
- }
- return this instanceof cls;
- }
整个实现也很清晰很简单,在第三章曾经描述过,不论Dojo中一个继承结构多么的复杂,归根结底还是一个单继承的形式,外加mixin进了许多类型的属性而已。那么在判断instanceof的时候,只要顺着这样一条继承链从低向高处遍历,沿途无论是发现了mixin的class,或者直接就是父类,这里都会返回true。
如果对JS中原生instanceof的判断机制感兴趣,可以参考
1. http://www.iteye.com/topic/461096
2. http://ejohn.org/blog/objectgetprototypeof
(四) chainedConstructor方法
第四章中提过,在declare中构建ctor的时候,针对不同的情况分别调用了三个函数。这里只研究其中的chainedConstructor,因为该函数最为复杂,另外两个simpleConstructor和singleConstructor函数不作详细分析。chainedConstructor是当declare的类型存在继承,且未设置constructor="manual"时调用的函数,返回值是一个用以充当declare类型的函数。
整个chainedConstructor可以看作三大步。第一步 就是就是执行在继承结构链上所有类型中定义的preamble函数,第二步 是调用所有类型的constructor函数,第三步 是执行当前类型中定义的postscript方法。如果撇开preamble和postscript,那么整个chainedConstructor的实现就只需要调用所有类型的constructor函数,相应的实现也可以被压缩成:
- // 遍历bases中的类型,未指明constructor的调用顺序,因此默认是after,即
- // 从bases[i]---->bases[0]依次调用constructor
- for(i = bases.length - 1; i >= 0; --i){
- f = bases[i];
- m = f._meta;
- f = m ? m.ctor : f;//可能是raw class
- if(f){
- f.apply(this, arguments);
- }
- }
可惜必须要面对存在preamble和postscript的情况,而且这两种情况的处理方式还不一样。其实preamble的调用也很简单,就是遍历bases,找出其中定义过的preamble函数,然后依次执行。
- function chainedConstructor(bases, ctorSpecial){
- return function(){
- var a = arguments, args = a, a0 = a[0], f, i, m,l = bases.length, preArgs;
- //如果不是利用new的时候调用该函数,那要强迫new出实例
- if(!(this instanceof a.callee)){
- // not called via new, so force it
- return applyNew(a);
- }
- // ctorSpecial=true仅发生在:没有定义chains,或者chains中的constructor
- // 一旦有了chains并且设置了constructor的顺序,则无需执行preamble
- // 有两种设置preamble的方式:
- // 一是在new的时候将preamble作为参数传递给该类型
- // 二是在declare一个类型的时候定义好
- if(ctorSpecial && (a0 && a0.preamble || this.preamble)){
- preArgs = new Array(bases.length);
- preArgs[0] = a;
- for(i = 0;;){
- // 如果是在参数中定义了preamble并传第给ctor
- a0 = a[0];
- if(a0){
- f = a0.preamble;
- if(f){
- a = f.apply(this, a) || a;
- }
- }
- // 如果是在类型的declare中定义preamble
- f = bases[i].prototype;
- f = f.hasOwnProperty("preamble") && f.preamble;
- if(f){
- a = f.apply(this, a) || a;
- }
- // for循环再遍历完了bases之后会结束
- if(++i == l){
- break;
- }
- //会记录下每次调用preamble之后的arguments
- //主要是为了防止某个preamble对arguments作出修改
- preArgs[i] = a;
- }
- }
- //第二步开始
- ……
- }
上面对第一步preamble的执行作出了一些解释。现在还剩最后一步,只针对当前的类型来调用postscript:
- f = this.postscript;
- if(f){
- f.apply(this, args);
- }
这个实在没什么好讲的。
从整个chainedConstructor的三大步骤实现来看,其实dojo的源码写的还是很通俗易懂的,结构也很清楚,是不错的学习材料^_^至此declare.js中比较重要的函数基本都已经讲完了,只缺少一个关于c3mro函数的剖析,但是前面讲mro已经花了大量的篇幅,便不打算再写下去了。以前都是仅仅停留在参阅Dojo的API的说明上,这是我第一次花力气去阅读Dojo的源码,可惜目前的工作中已经没有机会再使用Dojo了。