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之前还是之后生成。

 

 

 

你可能感兴趣的:(脚本,prototype,F#,dojo)