在第一章中讲到,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可以接受三种参数:
上面例子中的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的实现细节感兴趣,可以参考第四章。
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之前还是之后生成。