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
面向对象语言如果支持了多继承的话,都会遇到著名的菱形问题(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被先搜索到。
上面细述了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(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的处理方式与Python有一点点小区别。Python在构建对象的时候传入父类列表,越靠前的类越容易被搜索到,代表着对新建的类越重要。反之,如果一个父类处在越高的继承层次上,则越不容易被优先搜索到。dojo中的MRO大体上可以参考上节中的描述。但是略有区别,描述如下:
具体的区别已经在上面的算法描述中被标识出,可以看出,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算法可以很快的推导出来。
类似,左边的继承关系在Dojo中也应该是失败的。因为Dojo和Python中继承结构的线性化大体上是左右相反的。但实际上,无论是左边还是右边的继承关系,在Dojo中都是成功的。在Dojo中,分别针对左边和右边的继承进行MRO计算:
在Dojo中左边的继承能够MRO成功,主要原因是merge时传入的参数比Python中少了父类型序列。如下所示:
如果B1 ... BN之间(即父类型之间)彼此不存在继承关系,那么是否传入父类型序列对merge的结果是不造成影响的。但是如果B1 ... BN之间存在了继承关系,那么merge的时候,B1 ... BN将会结果造成直接影响。不传入父类型的序列,这正是Dojo中能够成功避免一些MRO失败的原因,也可以说,Dojo中的MRO并不像Python中那么严格。
Dojo中的MRO计算是通过c3mro函数来进行的,传入的参数是dojo.declare声明时的superclass数组。如果想知道c3mro实现的细节,可以参考第四章。
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。