Dojo面向對象機制深入剖析

Dojo是一個基於javascript語言的web控件庫,要了解dojo的面向對象機制,我們可以首先來回顧一下javascript本身的基本面向對象機制:

首先,javascript主要通過函數來模擬面向對象機制,如:

function func1(){};

func1(); //函數調用

new func1(); //實例化的對象

上述兩種方式其函數內部的this對象也是不一樣的。

再者,關於javascript的最基本的繼承方式主要有兩種,(當然還有更復雜的,這裏只列出最基本的兩種方式)

1. 對象冒充方式:

function ClassA(sColor) {
this.color = sColor;
this.xxx = xxx;
......
}

function ClassB(sColor, name) {
this.newMethod = ClassA;
this.newMethod(sColor);
delete this.newMethod;
......
}

或者:(這裏ClassA同上)

function ClassB(sColor, sName) {
ClassA.call(this, sColor); //或者apply

this.name = sName;
this.sayName = function() {
alert(this.name);
};
}

2. 原型鏈式繼承:

//基類

function ClassA() { }
ClassA.... = .....;
//子類
function ClassB() { }
//通過修改prototype來達到繼承目的
ClassB.prototype = new ClassA();

//定義新屬性與方法
ClassB.... = .....;

//保證constructor的一致性(修改prototype後其constructor變爲ClassA,應該是ClassB)

ClassB.prototype.constructor = ClassB;

上述兩種方法各有優缺點:對象冒充方式效率低,而且必須使用構造函數方式但是能支持多繼承,而prototype方式很靈活但不支持多繼承。於是有了如下混合方式:

function ClassA(sColor) { }
ClassA.... = ..... //設置屬性

function ClassB() {
ClassA.call(this, sColor); //對象冒充方式 繼承屬性
this.name = sName;
}
ClassB.prototype = new ClassA(); //原型鏈方式 繼承方法
ClassB.prototype.sayName = function() {
alert(this.name);
}

下面我們來看看dojo是如何實現繼承機制的:

下面是dojo中聲明類和使用類的一個簡單示例,可以看出,其核心在一個dojo.declare函數上:

我們來看看dojo.declare函數:

1> 先看第一個參數:className參數,可以見如下相關代碼:

......

// add name if specified
if(className){
proto.declaredClass = className;
d.setObject(className, ctor);
}

......

其中ctor是在該方法內構造的一個函數,其實是該聲明類的定義,其本身是一個函數對象,裏面有很多成員:包括屬性定義,函數定義,關於這個的詳細介紹這個後面會涉及到。這裏主要通過dojo.setObject將該構造的函數對象賦給名爲className的變量,如dijit.WidgetSet.

所以這裏相當於這樣寫:

ctor = function(){....};

Dijit.WidgetSet = ctor;

另外強調一下,這裏的proto就是ctor的prototype.

2> 我們再來看看第三個參數:props,可以見如下代碼:(具體可見加粗的字體說明)

............

proto = {};


//複製一份類的定義對象的內容,見上面Dijit.Calendar的templateString, value, buildRendering等等屬性和方法.
safeMixin(proto, props);



// 開始構造ctor,即構造類的定義,可以看到裏面還有很多dojo自己添加的屬性
t = !chains || !chains.hasOwnProperty(cname);

//以下三個對象singleConstructor/simpleConstructor/chainedConstructor 都是 是一個函數對象,其返回值也是一個函數對像,這些返回的函數對象內部,都會包含一些默認操作,包括初始化,調用ctor及其基類的ctor等等,具體細節可以參見上述三個函數的定義,可以看出模擬dojo的類的函數在初始化時都做了些什麼,很值得研究,爲了是篇幅不要太長,這裏暫時不做過於細緻的深入。
bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) :
(bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t));

// add meta information to the constructor

//這裏的ctor屬性很關鍵,在上面提到的 singleConstructor/simpleConstructor/chainedConstructor函數裏面會調用到,這也是爲什麼dojo.declare的「類」在初始化時(new操作)會自動調用constructor方法的原因!!!
ctor._meta = {bases: bases, hidden: props, chains: chains,
parents: parents, ctor: props.constructor };
ctor.superclass = superclass && superclass.prototype;
ctor.extend = extend;

//這裏是比較關鍵的操作了,將定義的屬性全部作爲prototype的屬性,注意我們這裏先不談繼承,而且這裏不是在繼承,請和前面的javascript繼承區分開

ctor.prototype = proto;

//由於prototype被修改,所以需要重新賦值以保持prototype的constructor一致性
proto.constructor = ctor;

// 添加dojo裏面比較常用的方法,注意:這些方法被直接加到了prototype中!
proto.getInherited = getInherited;
proto.inherited = inherited;
proto.isInstanceOf = isInstanceOf;

............


//最後將構造好的類對象ctor(本質上是一個函數對象)賦值給我們聲明的類名
dojo.setObject(className, ctor)

.............

所以這裏其實可以知道,在dojo中declare的類,其本質是函數對象,其實他就是如下代碼:

ctor = function(){};

ctor.prototype = props 屬性定義.....

類名變量{eval(className)} = ctor;

3> 下面我們來看一看第二個參數,關係着dojo的繼承機制:(具體可見加粗的字體說明)

..................

if(opts.call(superclass) == "[object Array]"){
//多個基類時,整理基類數組
bases = c3mro(superclass);
t = bases[0];
mixins = bases.length - t;

//基類數組裏的第一個對象
superclass = bases[mixins];
}else{

//只有一個基類的情況
bases = [0];
if(superclass){
if(opts.call(superclass) == "[object Function]"){
t = superclass._meta;
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?")
}
}

//開始構造基類屬性,合併他們的prototype內容
if(superclass){
for(i = mixins - 1;; --i){

//拷貝prototypesuperclass爲基類數組裏的第一個函數對象,forceNew之後變成含有superclass的prototype 屬性的一個對象
proto = forceNew(superclass);
if(!i){
// stop if nothing to add (the last base)
break;
}
// mix in properties
t = bases[i];

//合併所有基類的prototype,作爲後面子類的prototype,實現屬性共享
(t._meta ? mixOwn : mix)(proto, t.prototype);
//構造 基於合併prototype的基類臨時函數對象
ctor = new Function;
ctor.superclass = superclass;
ctor.prototype = proto;


superclass = proto.constructor = ctor;

//如上這行代碼用於調整所構造的ctor的constructor,保持一致性,然後修改superclass用於後續迭代,其實將這一行寫成如下兩行更通俗易懂:

// ctor.prototype.constructor = ctor;

// superclass = ctor;


}
}else{
proto = {};
}

.................

這裏我們只是粗略的把dojo.declare的主線路大致過了一遍,後面附上了dojo.declare的源代碼(基於dojo1.5),大家可以參考一下。關於dojo.declare的實現細節還有很多很多值得討論的地方,但是這裏由於篇幅過長,所以關於dojo的面向對象機制的詳細實現我們以後再繼續討論,這裏就不做詳細說明了。

dojo.declare 是和 dojo.require(類似java的import) 以及 dojo.provide 聯合起來一起使用的,主要由這3個函數將javascript封裝成了一個接近面向對象的dojo語言模式,這也是dojo比起其他web控件庫比較獨到的一大特點。

另外強調一點:在dojo.declare方法裏還實現了一種比較有用的鏈式機制,通過定義「-chains-」變量來使用,有興趣可以研究一下,這裏不做過多說明。

附上dojo.declare源代碼:(dojo1.5)

d.declare = function(className, superclass, props){
// crack parameters
if(typeof className != "string"){
props = superclass;
superclass = className;
className = "";
}
props = props || {};

var proto, i, t, ctor, name, bases, chains, mixins = 1, parents = superclass;

// build a prototype
if(opts.call(superclass) == "[object Array]"){
// C3 MRO
bases = c3mro(superclass);
t = bases[0];
mixins = bases.length - t;
superclass = bases[mixins];
}else{
bases = [0];
if(superclass){
if(opts.call(superclass) == "[object Function]"){
t = superclass._meta;
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?")
}
}
if(superclass){
for(i = mixins - 1;; --i){
proto = forceNew(superclass);
if(!i){
// stop if nothing to add (the last base)
break;
}
// mix in properties
t = bases[i];
(t._meta ? mixOwn : mix)(proto, t.prototype);
// chain in new constructor
ctor = new Function;
ctor.superclass = superclass;
ctor.prototype = proto;
superclass = proto.constructor = ctor;
}
}else{
proto = {};
}
// add all properties
safeMixin(proto, props);
// add constructor
t = props.constructor;
if(t !== op.constructor){
t.nom = cname;
proto.constructor = t;
}

// collect chains and flags
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-"]);
}

// build ctor
t = !chains || !chains.hasOwnProperty(cname);
bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) :
(bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t));

// add meta information to the constructor
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;

// add "standard" methods to the prototype
proto.getInherited = getInherited;
proto.inherited = inherited;
proto.isInstanceOf = isInstanceOf;

// add name if specified
if(className){
proto.declaredClass = className;
d.setObject(className, ctor);
}

// build chains and add them to the prototype
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;
}
}
}
// chained methods do not return values
// no need to chain "invisible" functions

return ctor; // Function };