dojo中使用dojo.declare来定义类,它有如下三个参数:
className: 表示类名(即构造函数)的字符串,通常会在全局空间创建这个类,也可以将类名用点分割,如: "myNamespace.Shape",那么Shape就会最为对象myNamespace的属性,如果myNamespace不存在,那么myNamespace.Shapde就会作为全局空间的属性。
superclass: 可选项(null, 函数或者函数数组),指明这个类的父类或者要聚合的类。
props: 一个包含了属性的对象自变量,用来初始化类的原型。如果这个对象自变量中含有一个叫constructor的函数,那么这个函数用来初始化对象实例,叫做初始化函数。
下面用它来创建一个Shape类:
dojo.declare("Shape", null, {color: 0, setColor: function(color) {this.color = color;}});
创建一个Shape对象: var s = new Shpae();
Shape类包含下面属性:
superclass:指向父类原型,如果没有就为undefined。
mixin: 指向聚合类的原型,如果没有就为undefiened。
extend: 一个高级应用,下面要讲。
_constructor: 指向类的初始化函数,这个函数负责构造所有的实例属性,当创建新对象时,有构造函数自动调用。
declaredClass: 就是传给dojo.declare的第一个参数。
inherited: 指向一个用于调用父类方法的函数
使用单继承定义类
为了是子类继承父类,只要将dojo.declare的第二个参数设置为父类的构造函数就可以了:
/*Circle继承自Shape*/ dojo.declare("Circle", Shape, { radius: 0; /*名为constructor的函数来初始化新实例,再调用构造函数的时候被自动调用*/ constructor: function(radius) { this.radius = radius || 0; }, setRadius: function(radius) { this.radius = radius; } });
当创建一个Circle对象时,dojo会做两件事:
1 调用父类构造函数
2 调用子类自身的初始化函数
var circle = new Circle(5); /*调用父类构造函数*/ Shape.apply(this, arguments); /*调用子类初始化函数*/ this._constructor.apply(this, arguments);
Circle类的原型会指向Shape的原型。
重载父类的方法
子类通常会覆写父类的方法,而且还会在覆写的方法中调用父类的方法,可以通过inherited方法实现这个操作。
/*Circle继承自Shape*/ dojo.declare("Circle", Shape, { radius: 0; /*名为constructor的函数来初始化新实例,再调用构造函数的时候被自动调用*/ constructor: function(radius) { this.radius = radius || 0; }, setRadius: function(radius) { this.radius = radius; }, /*覆写父类中的setColor方法*/ setColor: function(color) { if (color > 100) { this.inherited(arguments); //调用父类中的版本 } } });
inherited会找到父类中的方法,然后通过下面的形式调用:
1.function inherited(args, a, func){ 2. // crack arguments 3. if(typeof args == "string"){ 4. name = args; 5. args = a; 6. a = func; 7. } 8. // find method f 9. …… 10. if(f){ 11. return a === true ? f : f.apply(this, a || args); 12. } 13.}
inherited接受三个参数:
methodName: 要调用的方法名,可选。
args: 即arguments字面量,必选
a: 额外的参数数组,若有这个参数,父类中的方法就不接受arguments字面量。可选
向父类添加方法
向由dojio.declare创建的类的原型中添加方法,应该调用类自带的extend方法,而向其他非dojo.declare创建的构造函数原型中添加方法,调用dojo.extend.
聚合与多继承
dojo.declare可以模拟多继承,这是以聚合的方式实现的。比如现在有一个Circle类,有一个Position类,我们要创建一个新类继承Circle类,聚合Position类:
dojo.declare("CirclePosition", [Circle, Position], { constructor: function(radius, x, y) { this.setPosition(x, y); } });
这里,dojo.declare的第二个参数为一个构造函数数组,其中的第一个构造函数会被继承,后面的构造函数会被聚合。
而当我们实例化一个CirclePosition类时,会执行下列操作:
var circlePosition = new CriclePosition(5, 4, 3); /*父类构造函数被调用*/ Circle.apply(this, arguments); /*父类的构造函数被调用*/ Shape.apply(this, arguments); /*聚合类构造函数被调用*/ Position.apply(this, arguments); /*类本身的初始化函数被调用*/ CirclePosition._constructor.apply(this, arguments);
下面是CirclePosition对象空间
通过这个对象空间可以发现,CirclePosition的原型对象(6)并没有直接指向Circle的原型对象(7),而是指向了一个中间构造函数的原型对象(4),这个原型对象才指向了Circle的原型对象,同时,Position原型对象中的属性和方法都被复制到了这个原型对象中(其中基本类型是值复制,而方法以及对象是引用复制,并且是所有的属性,包括Position继承来的)。
从这里还要注意,Position的原型对象不在CirclePosition的原型链中,并且聚合的属性很可能覆盖掉更低级别的原型对象中的同名属性。
预处理构造函数参数
如果类自身,其父类,其聚合的类的构造函数的参数签名不一样,那么,在初始化一个类实例的时候很可能出问题。如:CirclePosition继承自Circle类,Circle类继承自Shape类,同时CriclePosition类聚合Position类。其中CirclePosition的构造函数签名(radius, x, y), Circle构造函数的签名(radius), Shape构造函数的签名(), Position构造函数的签名(x, y)。那么当实例化一个CirclePosition时:
var circlePosition = new CirclePosition(5, 4, 3); /*下面的函数调用将依次执行*/ Circle.apply(this, [5, 4, 3]); Shape.apply(this, [5, 4, 3]); Position.apply(this, [5, 4, 3]); CirclePosition.prototype._constructor.apply(this, [5, 4, 3]);
由于Circle只接受radius参数,因此能够被正常初始化,Shape类不接受任何参数,因此也能够正常初始化,而Position接受x,y作参数,因此,它会将radius当成x,把x当成y,不能够正常初始化。但是我们可以将Circle的初始化函数定义成:
function constructor(raduis, x, y) { this.setPosition(x, y);//setPosition是Position类中的方法,用来设置x, y的值 }
这样,尽管Position构造函数初始化不正确,但是CirclePosition的初始化函数还是可以正确初始化。使用这种方法大多数情况下不会出现问题,但有时候也会跑出异常。有两种方法可以解决这个问题:
1 接受预处理器
dojo.declare接受一个预处理器(即一个函数),这个预处理器定义在传递给它的但三个参数里面,这个预处理器被定义为一个叫preamble的属性。这个预处理器会在参数传递个父类或者聚合类的构造函数时,先对参数进行格式化,以产生适合父类或者构造函数的参数。当时单继承或者没有聚合类,或者聚合类和父类的参数都一样的时候,这中方法可以工作的很好。但是当聚合类和父类的构造函数参数不一致的时候,就会出问题,因为这个预处理器格式化的参数要么只适合父类,要么只适合聚合类。为了解决这个问题,我们可以创建一个夹层类(Shim Class),以上面的CriclePosition为例,首先创建一个夹层类,它集成自Position类:
dojo.declare("PositionShimClass", Position, { preamble: funciton(raduis, x, y) { //预处理器 return [x || null, y || null]; } }
里面的预处理器将为Position格式化参数。
然后用这个PositionShimClass来聚合CirclePosition:
dojo.declare("CirclePosition", [Circle, PositionShimClass], {});
当此时在创建CirclePosition的实例时:
var circlePosition = new CirclePosition(5, 4, 3); /*下面的函数调用将依次执行*/ Circle.apply(this, [5, 4, 3]); Shape.apply(this, [5, 4, 3]); PositionShimClass.apply(this, PositionShimClass.prototype.preamble.apply(this, [5, 4, 3]); //预处理后的参数将传递给Position构造函数 if (CirclePosition.prototype._constructor) {//如果初始化函数存在 CirclePosition.prototype._constructor.apply(this, [5, 4, 3]); }
这样就能正确初始化。也就是说,子类中的参数顺序负责向父类构造函数提供正确的参数,而夹层类负责聚合类提供正确的参数。
2 用对象字面量来传递参数
var circlePosition = new CirclePosition({ radius: 5, x: 4, y: 3 }); /*Circle类中的初始化函数*/ function constructor(args) { if (args && args.radius) { this.radius = args.radius; } } /*Position类中的初始化函数*/ function constructor(args) { if (args && args.x && args.y) { this.x = args.x; this.y = args.y; } }
使用这种方式,可以不用遵循参数的顺序,而且总是安全的。这种方法适合有多个参数的情况。
解决名字冲突:
聚合类中的所有属性被添加到了新类的原型链中,而这些属性在原型链中的位置位于父类之上,因此当出现同名属性时,很可能会覆盖掉父类中的属性。我们仍以CirclePosition,Circle, Shape, Position为例。CirclePosition继承自Cricle, Circle继承自Shape,同时CriclePosition聚合Position。Circle中有setRadius方法,Shape中有setColor方法,Position中有setPosition方法。如果以这种方式命名,他们不会产生冲突,但是如果他们都命名为set,那么就会产生冲突。当CirclePosition的实例调用set方法时,只有Position中的set方法被调用。解决这种问题的方法是给Position的set方法重命名:
CirclePosition.extend({ setPosition: Position.prototype.set }); delete CirclePosition.superclass.set;
那么接下来如何消除Cricle.set和Shape.set的歧义呢?这其实是设计上的错误,应该再设计中就为他们叫一个合理的名字。
两阶段构造
假设有这样的一个几类Base:
dojo.declare("Base", null, { constructor: function() { this.args = {baseArags: dosomething(this); } }
这个基类中的初始化函数要求实例完全构造完成之后调用才行,如果从这个基类派生出一个子类subclass,由于父类的初始化函数在子类初始化函数完成之前被调用,因此这个父类初始化函数将不能正常工作。因此我们需要保证子类已经被完全构造,再来调用这个方法,而dojo.declare的第三个对象字面量参数提供postscript属性来完成这个操作。它保证这个属性定义的方法会在所有方法被调用之后(包括父类构造器方法,聚合类构造器方法,子类自身的初始化方法),这个方法才在新实例上执行。这就是两阶段构造。
使用postscript必须注意,它仅仅被最终派生出来的类调用,如果父类定义了这个方法,而子类没有定义,那么调用的就是父类的方法;如果子类覆写了这个方法,那么可以通过this.inherited(arguments)实现,并且这个方法接受的参数是传递给构造器的参数。
下面我们来看一个例子,父类和子类将会把它们接受的字符存到args变量中,然后再将他们全部输出来。首先不适用postscript:
/*父类*/ dojo.declare("Base", null, { constructor: function() { this.args = {baseArgs: dojo._toArray(arguments)}; console.log(dojo.toJson(this.args, true)); }); /*子类*/ dojo.declare("Subclass", Base, { constructor: function() { this.args.subArags = dojo._toArray(arguments); }, preamble: function() { return dojo._toArray(arguments).slice(0, 3); //父类只得到其中的三个参数 } }); var subClass = new Subclass(1, 2, 3, 4, 5, 6);
这导致父类输出时,将漏掉传递给子类的参数。
现在修改如下:
ojo.declare("Base", null, { constructor: function() { this.args = {baseArgs: dojo._toArray(arguments)}; }, postscript: function() { console.log(dojo.toJson(this.args, true)); } ); dojo.declare("Subclass", Base, { constructor: function() { this.args.subArgs = dojo._toArray(arguments); }, postscript: function() { console.log("in subclass"); this.inherited(arguments); }, preamble: function() { return dojo._toArray(arguments).slice(0, 3); } });
不适用构造函数产生自定义对象:
dojo.delegate(obj, propt): 创建并返回一个新对象,并利用dojo.mixin将propt合并到新对象中:
var Base = { greet: function() { return "Hello, my Name is " + this.name; } }; var newObject = dojo.delegate(Base, {name: "Jone"});
当调用newObject.greet()时产生"Hello, my name is Jone".