Dojo

dojo的類機制支持類聲明、繼承、調用父類方法等功能。dojo在底層實現上是經過操做原型鏈來實現其類機制的,而在實現繼承時採用類式繼承的方式。值得一提的是,dojo的類機制容許進行多重繼承(注意,只有父類列表中的第一個做爲真正的父類,其它的都是將其屬性以mixin的方法加入到子類的原型鏈中),爲解決多重繼承時類方法的順序問題,dojo用JavaScript實現了Python和其它多繼承語言所支持的C3父類線性化算法,以實現線性的繼承關係,想了解更多該算法的知識,可參考這裏,咱們在後面的分析中將會簡單講解dojo對此算法的實現。javascript

1.dojo類聲明概覽

dojo類聲明相關的代碼位於「/dojo/_base/declare.js」文件中,定義類是經過dojo.declare方法來實現的。關於這個方法的基本用法,已經在dojo類機制簡介這篇文章中進行了闡述,如今咱們看一下它的實現原理(在這部分的代碼分析中,會在總體上介紹dojo如何聲明類,後文會對裏面的重要細節內容進行介紹):html

 
//此即爲dojo.declare方法的定義
d.declare = function(className, superclass, props){

    //前面有格式化參數相關的操做,通常狀況下定義類會把三個參數全傳進來,分別爲
    //類名、父類(能夠爲null、某個類或多個類組成的數組)和要聲明類的屬性及方法

    //定義一系列的變量供後面使用
    var proto, i, t, ctor, name, bases, chains, mixins = 1, parents = superclass;

    // 處理要聲明類的父類
    if(opts.call(superclass) == "[object Array]"){
    //若是父類參數傳過來的是數組,那麼這裏就是多繼承,要用C3算法處理父類的關係
        //獲得的bases爲數組,第一個元素能標識真正父類(即superclass參數中的第一個)
        //在數組中的索引,其他的數組元素是按順序排好的繼承鏈,後面還會介紹到C3算法
    bases = c3mro(superclass, className);
    t = bases[0];
    mixins = bases.length - t;
    superclass = bases[mixins];
    }else{
    //此分支內是對沒有父類或單個父類狀況的處理,再也不詳述
    }
    //如下爲構建類的原型屬性和方法
    if(superclass){
    for(i = mixins - 1;; --i){
            //此處遍歷全部須要mixin的類
            //注意此處,爲何說多個父類的狀況下,只有第一個父類是真正的父類呢,由於在第一次循環的實例化了該父類,並記在了原型鏈中,而其它須要mixin的
            //父類在後面處理時會把superclass設爲一個空的構造方法,合併父類原型鏈
            //後進行實例化
            proto = forceNew(superclass);
            if(!i){
                //此處在完成最後一個父類後跳出循環
                break;
             }
            // mix in properties
            t = bases[i];//獲得要mixin的一個父類
            (t._meta ? mixOwn : mix)(proto, t.prototype);//合併原型鏈
            // chain in new constructor
            ctor = new Function;//聲明一個新的Function
            ctor.superclass = superclass;
            ctor.prototype = proto;//設置原型鏈
            //此時將superclass指向了這個新的Function,再次進入這個循環的時候,實例//化的是ctor,而不是mixin的父類
            superclass = proto.constructor = ctor; 
    }
    }else{
    proto = {};
    }
    //此處將上面獲得的方法(及屬性)與要聲明類自己所擁有的方法(及屬性)進行合併
    safeMixin(proto, props);
    
    …………
    //此處收集鏈式調用相關的信息,後面會詳述
    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-"]);
    }
    
    //此處根據上面收集的鏈式調用信息和父類信息構建最終的構造方法,後文詳述
    t = !chains || !chains.hasOwnProperty(cname);
    bases[0] = ctor = (chains && chains.constructor === "manual") ? simpleConstructor(bases) :
    (bases.length == 1 ? singleConstructor(props.constructor, t) : chainedConstructor(bases, t));

    //在這個構造方法中添加了許多的屬性,在進行鏈式調用以及調用父類方法等處會用到
    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;

    // 對於dojo.declare方法聲明類的實例均有如下的工具方法
    proto.getInherited = getInherited;
    proto.inherited = inherited;
    proto.isInstanceOf = isInstanceOf;

    // 此處要進行全局註冊
    if(className){
    proto.declaredClass = className;
    d.setObject(className, ctor);
    }

    //對於鏈式調用父類的那些方法進行處理,實際上進行了重寫,後文詳述
    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;
            }
    }
    }
    return ctor;	// Function
};

以上簡單介紹了dojo聲明類的總體流程,可是一些關鍵的細節如C3算法、鏈式調用在後面會繼續進行介紹。java

2.C3算法的實現

經過之前的文章和上面的分析,咱們知道dojo的類聲明支持多繼承。在處理多繼承時,不得不面對的就是繼承鏈如何構造,比較現實的問題是若是多個父類都擁有同名的方法,那麼在調用父類方法時,要按照什麼規則肯定調用哪一個父類的呢?在解決這個問題上dojo實現了C3父類線性化的方法,對多個父類進行合理的排序,從而完美解決了這個問題。python

爲了瞭解繼承鏈的相關知識,咱們看一個簡單的例子:編程

dojo.declare("A",null);
dojo.declare("B",null);
dojo.declare("C",null);
dojo.declare("D",[A, B]);
dojo.declare("E",[B, C]);
dojo.declare("F",[A, C]);
dojo.declare("G",[D, E]);

以上的代碼中,聲明瞭幾個類,經過C3算法獲得G的繼承順序應該是這樣G->E->C->D->B->A的,只有按照這樣的順序才能保證類定義和依賴是正確的。那咱們看一下這個C3算法是如何實現的呢:數組

function c3mro(bases, className){
    //定義一系列的變量
    var result = [], roots = [{cls: 0, refs: []}], nameMap = {}, clsCount = 1,
        l = bases.length, i = 0, j, lin, base, top, proto, rec, name, refs;

    //在這個循環中,構建出了父類各自的依賴關係(即父類可能會依賴其它的類)
    for(; i < l; ++i){
        base = bases[i];//獲得父類
        …………
        //在dojo聲明的類中都有一個_meta屬性,記錄父類信息,此處可以獲得包含自己在//內的繼承鏈
        lin = base._meta ? base._meta.bases : [base];
        top = 0;
        for(j = lin.length - 1; j >= 0; --j){
            //遍歷繼承鏈中的元素,注意,這裏的處理是反向的,即從最底層的開始,一直到鏈的頂端
            proto = lin[j].prototype;
            if(!proto.hasOwnProperty("declaredClass")){
                proto.declaredClass = "uniqName_" + (counter++);
            }
            name = proto.declaredClass;
            // nameMap以map的方式記錄了用到的類,不會重複
            if(!nameMap.hasOwnProperty(name)){
                //每一個類都會有這樣一個結構,其中refs特別重要,記錄了引用了依賴類
                nameMap[name] = {count: 0, refs: [], cls: lin[j]};
                ++clsCount;
            }
            rec = nameMap[name];
            if(top && top !== rec){
                //知足條件時,意味着當前的類依賴此時top引用的類,即鏈的前一元素
                rec.refs.push(top);
                ++top.count;
            }
            top = rec;//top指向當前的類,開始下一循環
        }
        ++top.count;
        roots[0].refs.push(top);//在一個父類處理完成後就將它放在根的引用中
    }
    //到此爲止,咱們創建了父類元素的依賴關係,如下要正確處理這些關係
    while(roots.length){
        top = roots.pop();
        //將依賴的類插入到結果集中
        result.push(top.cls);
        --clsCount;
        // optimization: follow a single-linked chain
        while(refs = top.refs, refs.length == 1){
            //若當前類依賴的是一個父類,那處理這個依賴鏈
            top = refs[0];
            if(!top || --top.count){
                //特別注意此時有一個top.count變量,是用來記錄這個類被引用的次數,
				//若是減一以後,值還大於零,就說明後面還有引用,此時不作處理,這也就是
				//在前面的例子中爲何不會出現G->E->C->B的緣由
                top = 0;
                break;
            }
            result.push(top.cls);
            --clsCount;
        }
        if(top){
	        //若依賴多個分支,則將依賴的類分別放到roots中,這段代碼只有在多繼承,//第一次進入時纔會執行
            for(i = 0, l = refs.length; i < l; ++i){
                top = refs[i];
                if(!--top.count){
                    roots.push(top);
                }
            }
        }
    }
    if(clsCount){
        //若是上面處理完成後,clsCount的值還大於1,那說明出錯了
        err("can't build consistent linearization", className);
    }

    //構建完繼承鏈後,要標識出真正父類在鏈的什麼位置,就是經過返回數組的第一個元素
    base = bases[0];
    result[0] = base ?
    base._meta && base === result[result.length - base._meta.bases.length] ?
        base._meta.bases.length : 1 : 0;

    return result;
}

經過以上的分析,咱們能夠看到,這個算法實現起來至關複雜,若是朋友們對其感興趣,建議按照上文的例子,本身加斷點進行調試分析。dojo的做者使用了不到100行的代碼實現了這樣強大的功能,裏面有不少值得借鑑的設計思想。瀏覽器

3. 鏈式構造器的實現

在第一部分代碼分析中咱們曾經看到過定義構造函數的代碼,以下:

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

這個方法對與理解dojo類機制很重要。從前一篇文章的介紹中,咱們瞭解到默認狀況下,若是dojo聲明的類存在繼承關係,那麼就會自動調用父類的構造方法,且是按照繼承鏈的順序先調用父類的構造方法,可是從1.4版本開始,dojo提供了手動設置構造方法調用的選項。在以上的代碼中涉及到dojo聲明類的三個方法,若是該類沒有父類,那麼調用的就是singleConstructor,若是有父類的話,那麼默認調用的是chainedConstructor,若是手動設置了構造方法調用,那麼調用的就是simpleConstructor,要啓動這個選項只需在聲明該類的時候添加chains的constructor聲明便可。

比方說,咱們在定義繼承自com.levinzhang.Person的com.levinzhang.Employee類時,能夠這樣作:

dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{
    "-chains-": {
        constructor:"manual"
    },
    …………
}

添加以上代碼後,在構造com.levinzhang.Employee實例時,就不會再調用全部父類的構造方法了,可是此時咱們能夠使用inherited方法顯式的調用父類方法。

限於篇幅,以上的三個方法不所有介紹,只介紹chainedConstructor的核心實現:

function chainedConstructor(bases, ctorSpecial){ 
    return function(){ 
        //在此以前有一些準備工做,不詳述了 
        //找到全部的父類,分別調用其構造方法 
        for(i = l - 1; i >= 0; --i){ 
            f = bases[i]; 
            m = f._meta; 
            f = m ? m.ctor : f;
            //獲得父類的構造方法 
            if(f){ 
                //經過apply調用父類的方法 
                f.apply(this, preArgs ? preArgs[i] : a); 
            } 
        } 
        // 請注意在構造方法執行完畢後,會執行名爲postscript的方法,而這個方法是
        //dojo的dijit組件實現的關鍵生命週期方法 
        f = this.postscript; if(f){ 
            f.apply(this, args); 
        } 
    }; 
}

4. 調用父類方法的實現

在聲明dojo類的時候,若是想調用父類的方法通常都是經過使用inherited方法來實現,但從1.4版本開始,dojo支持鏈式調用全部父類的方法,並引入了一些AOP的概念。咱們將會分別介紹這兩種方式。

經過inherited方式調用父類方法

在上一篇文章中,咱們曾經介紹過,經過在類中使用inherited就能夠調用到。這裏咱們要深刻inherited的內部,看一下它的實現原理。由於inherited支持調用父類的通常方法和構造方法,二者略有不一樣,咱們關注調用通常方法的過程。

function inherited(args, a, f){
    …………
    //在此以前有一些參數的處理
    if(name != cname){
        //不是構造方法
        if(cache.c !== caller){
            //在此之間的一些代碼解決了肯定調用者的問題,即肯定從什麼位置開始找父類
        }
        //按照順序找父類的同名方法
        base = bases[++pos];
        if(base){
            proto = base.prototype;
            if(base._meta && proto.hasOwnProperty(name)){
                f = proto[name];//找到此方法了
            }else{
                //若是沒有找到對應的方法將按照繼承鏈依次往前找
                opf = op[name];
                do{
                    proto = base.prototype;
                    f = proto[name];
                    if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){
                        break;
                    }
                }while(base = bases[++pos]); // intentional assignment
            }
        }
        f = base && f || op[name];
    }else{
        //此處是處理調用父類的構造方法
    }
    if(f){
        //方法找到後,執行
        return a === true ? f : f.apply(this, a || args);
    }
}

鏈式調用父類方法

這是從dojo 1.4版本新加入的功能。若是在執行某個方法時,也想按照必定的順序執行父類的方法,只需在定義類時,在-chains-屬性中加以聲明便可。

dojo.declare("com.levinzhang.Employee", com.levinzhang.Person,{
    "-chains-": {
     sayMyself:    "before"
	},
    ……
}

添加了以上聲明後,意味着Employee及其全部的子類,在調用sayMyself方法時,都會先調用自己的同名方法,而後再按照繼承鏈依次調用全部父類的同名方法,咱們還能夠將值「before」替換爲「after」,其執行順序就會反過來。在-chains-屬性中聲明的方法,在類定義時,會進行特殊處理,正如咱們在第一章中看到的那樣:

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;
        }
    }
}

咱們能夠看到,在-chains-中聲明的方法都進行了替換,換成了chain方法的返回值,而這個方法也比較簡單,源碼以下:

function chain(name, bases, reversed){
    return function(){
        var b, m, f, i = 0, step = 1;
        if(reversed){
            //斷定順序,即"after"仍是"before",分別對應於循環的不一樣起點和方向
            i = bases.length - 1;
            step = -1;
        }
        for(; b = bases[i]; i += step){ 
            //按照順序依次查找父類
            m = b._meta;
            //找到父類中同名的方法
            f = (m ? m.hidden : b.prototype)[name];
            if(f){
                //依次執行
                f.apply(this, arguments);
            }
        }
    };
}

5.工具方法和屬性如isInstanceOf、declaredClass的實現

除了上面提到的inherited方法之外,dojo在實現類功能的時候,還實現了一些工具方法和屬性,這裏介紹一個方法isInstanceOf和一個屬性declaredClass。從功能上來講isInstanceOf方法用來判斷一個對象是否爲某個類的實例,而declaredClass屬性獲得的是某個對象所對應聲明類的名字。

function isInstanceOf(cls){
    //獲得實例對象繼承鏈上的全部類
    var bases = this.constructor._meta.bases; 
    //遍歷全部的類,看是否與傳進來的類相等
    for(var i = 0, l = bases.length; i < l; ++i){
        if(bases[i] === cls){
            return true;
        }
    }
    return this instanceof cls;
}

而declaredClass屬性的實現比較簡單,只是在聲明類的原型上添加了一個屬性而已,類的實例對象就能夠訪問這個屬性獲得其聲明類的名字了。這段代碼在dojo.declare方法中:

if(className){
    proto.declaredClass = className;
    d.setObject(className, ctor);
}

在dojo實現類機制的過程當中,有一些內部的方法,是很值得借鑑的,如forceNew、safeMixin等,這些方法在實現功能的同時,保證了代碼的高效執行,感興趣的朋友能夠進一步研究。

6.總結與思考

  1. dojo在實現類機制方面支持多繼承方式,其它JavaScript類庫中不多能作到,而利用JavaScript原生語法實現多繼承也較爲困難。在這一點上dojo的類機制的功能確實足夠強大。可是多繼承會增長編碼的難度,對開發人員如何組織類也有更高的要求;
  2. 鏈式調用父類方法時,咱們能夠看到dojo引入了許多AOP的理念,在1.7的版本中,將會有單獨的模塊提供AOP相關的支持,咱們將會持續關注相關的功能;
  3. 在dojo的代碼中,多處都會出現方法替換,如鏈式方法調用、事件綁定等,這種設計思想值得咱們關注和學習;
  4. 使用了許多的內部屬性,如_meta、bases等,這些元數據在實現複雜的類機制中起到了相當重要的做用,在進行源碼分析的時候,咱們能夠給予關注,若是要實現相似功能也能夠進行借鑑。

探究類庫的實現原理是提升本身編碼水平的好辦法,相似於dojo這樣類庫的核心代碼基本上每一行都有其設計思想在裏面(固然也不能夠盲目崇拜),每次閱讀和探索都會有所發現和心得,固然裏面確定也會有自覺得是或謬誤之處,在此很樂意和讀到這篇文章的朋友們一塊兒研究,歡迎批評指正。

參考資料:

關於做者

張衛濱,關注企業級Java開發和RIA技術,我的博客:http://lengyun3566.iteye.com,微博:http://weibo.com/zhangweibin1981


DOJO中的面向對象

 

  在JS中並無Java等面嚮對象語言中常見的類(class)的概念。也就是說,JS中的對象並不是基於類的。它僅僅爲開發者提供了一些原類型和基本的內置對象。從寫法上來看,它更加的相似於面向過程式的語言,函數彷彿成了JS中的頂級實體。事實上,JS是一門函數式編程的語言。因此當咱們須要以面向對象的方式來構建大型web應用時,原生態的JS並不能很好的知足這一點。而DOJO的出現完美的解決了這個問題,它使得程序員可以以傳統的面向對象的思惟來進行開發,進而使JS用起來更加駕輕就熟。

 

第一章 JS中的對象模型

(一) 構造器(僞類)

  在JS中建立一個對象很容易,JS提供了語法糖容許咱們以字面量的方式來構建對象。例如:

Js代碼   收藏代碼
  1. var foo={   'name':'Jack' }  

 

  可是在JS中構建一個類卻顯得稍微複雜。因爲JS並無從語言層面上支持任何自定義類型,因此咱們只能經過模擬的方式來構建出一個類。這得益於JS中的強大的函數以及原型機制。先來看一個簡單的例子:

Js代碼   收藏代碼
  1. function Foo(){  
  2.     this.name='Jack';  
  3. }  
  4. var foo=new Foo();  
  5. console.log(foo.name) // 輸出jack  

 

  在這個例子中,Foo已經不只僅是一個簡單的函數了,咱們將Foo當作一個‘僞類’(在javascript中稱爲構造器)。所以能夠用new運算符來生成該類型的對象。一般JS中的類型構造都採用該方法。新生成的對象將自動擁有‘僞類’中定義的字段,因此此例中生成的foo將擁有name屬性。

 

  注意Foo中的this.name='Jack'。因爲JS中的某些不良設計,通常的函數調用會將該函數中的this綁定到全局對象上,這使得this的使用容易形成混亂。一般而言,若是並不涉及到面向對象編程,能夠沒必要使用this。只有存在了某個對象,this的使用纔會有意義。


  若是對構造器進行new運算,構造器中的this會被綁定到生成的新的對象。換句話說,上例中new Foo()時,Foo中的this會被綁定到新生成的實例foo。能夠猜想,對一個Foo調用new運算符的時候,會發生相似於下面的過程: 

Js代碼   收藏代碼
  1. var obj=new Object();   //obj是一個新生成的對象  
  2. Foo.apply(obj);         //將Foo中的this綁定到obj  
  3. var foo=obj;            //最後將obj引用返回給foo  
 

(二) prototype是什麼

  JS中的繼承有點特殊,在JS中並不存常見的基於類的繼承。JS中的繼承是對象與對象之間的行爲。也就是說,一個對象能夠直接繼承另外一個對象裏的屬性。而這一切,都是依靠prototype來完成的。示例以下:

Js代碼   收藏代碼
  1. var foo={  
  2.     'name':'Jack'  
  3. }  
  4. function Bar(age){  
  5.     this.age=age;  
  6. }  
  7. Bar.prototype=foo;  
  8. var bar=new Bar(22);  

 

  這個例子中,咱們首先建立了一個對象foo,它包含一個name屬性。而後咱們建立了一個構造器Bar,因爲將Bar當作類來使用,因此將其首字母大寫。隨後咱們將Bar的原型指向foo對象。接着,咱們以new的方式來建立了一個Bar的實例bar。很顯然,對象bar中包含了兩個屬性,name屬性值爲Jack,還有age屬性,值爲22。值得考究的是Bar. prototype=foo這一句。該語句將Bar的原型設定成一個對象foo。這一句的運行結果是經過Bar建立的全部對象都將繼承foo對象的屬性。因而,接下來bar便從foo中繼承了name屬性。

 

  推廣開來講, JS中的每一個構造器都有一個prototype屬性。JS裏的構造器,除了包括了咱們上面本身定義的‘僞類’,還包括了內置的Object、Function、String 、Array、Number、Date、RegExp等等一系列函數。prototype自己也是一個對象,也就是咱們所說的原型對象,若是咱們用構造器建立了一個新的對象,該對象便與原型對象發生了繼承關係。注意,JS中的繼承是發生在兩個對象之間的關係,而JAVA之中的繼承是兩個類之間的關係。


  JS中的繼承有兩個突出的特色,一是動態性,二是對修改封閉。下面的例子闡述了這兩點,例一:

Js代碼   收藏代碼
  1. var foo={ 'name':'Jack' }             
  2. function Bar(age){  
  3.     this.age=age;  
  4. }             
  5. Bar.prototype=foo;            
  6. var bar=new Bar(22);  
  7. console.log(bar.name)    //Jack  
  8. foo.name='Mike';  
  9. console.log(bar.name)    //Mike  

 

  當咱們修改了foo的屬性時,經過bar來訪問這些屬性也會收到影響。也就是說,咱們能夠將一些須要的特性動態添加到JS的對象中。這是一種很是強大的編程技術。好比JS中的String對象缺乏trim方法。經過

Js代碼   收藏代碼
  1. String.prototype.trim=function(){//dojo中的實現  
  2.   return this.replace(/^\s\s*/,'').  
  3.   replace(/\s\s*$/,'');  
  4. }   

 

語句,能夠爲全部的string加上trim方法。

例二:

Js代碼   收藏代碼
  1. var foo={ 'name':'Jack' }  
  2. function Bar(age){  
  3.     this.age=age;  
  4. }  
  5. Bar.prototype=foo;  
  6. var bar=new Bar(22);  
  7. bar.name='Mike';  
  8. console.log(bar.name)    // Mike  
  9. console.log(foo.name)    // Jack  

 

  從上例中能夠清楚的看出,若是咱們試圖經過修改bar來影響foo,這樣是行不通的。經過bar能夠訪問foo的屬性,可是卻沒法改變這些屬性的值。當咱們修改bar.name='Mike'以後,foo的name值依然是Jack。

 

(三) 原型鏈(prototype chain)

  事實上,在bar對象中,有一個看不見的_proto屬性。該屬性指向了Bar.prototype,也就是foo。在Ecma-262 3rd Edition中有以下描述:

寫道
Each constructor has a Prototype property that is used to implement prototype-based inheritance and shared properties.
寫道
Every constructor has an associated prototype, and every object created by that constructor has an implicit reference to the prototype associated with its constructor.

 

  這段話的意思是JS中的構造器都擁有一個prototype屬性。每一個由構造器建立出來的object都含有一個指向該prototype的隱式引用。

Js代碼   收藏代碼
  1. function Foo(){  
  2.     this.name='Jack';  
  3. }  
  4. var foo=new Foo();  
  5. function Bar(age){  
  6.     this.age=age;  
  7. }  
  8. Bar.prototype=foo;  
  9. var bar=new Bar(22);  

 

所以,上例能夠表示成:

 

  注意綠色虛線框內的部分。經過_proto,能夠將許多對象串起來,造成一個鏈條,也就是咱們常常說的原型鏈(prototype chain)。當咱們試圖訪問對象中的一個屬性,首先會在對象本體中尋找該屬性。若是沒有找到,JS會自動在該對象的原型對象中查詢該屬性,這個過程是一種上溯。若是仍是沒有找到,會繼續上溯到原型對象的原型對象中。


  可是這個步驟不是無止盡的,這個上溯的過程直到Object.prototype._proto爲止。以上面的圖爲例,從foo能夠找到的原型對象是Foo.prototype。Foo.prototype自己是一個對象,也是Object的一個實例,所以有:Foo.prototype._proto=Object.prototype 。


  因此在向上追溯的過程當中,會追溯到Object.prototype這個對象。若是依然沒有咱們要找的屬性,那還會繼續向上追溯麼?從Ecma-262 3rd Edition15.2.4節中能夠得知:

寫道
The value of the internal [[Prototype]] property of the Object prototype object is null and the value of the internal [[Class]] property is "Object".

 

  也就是說Object.prototype._proto=null.至此,能夠清楚的弄明白,整個原型鏈的最頂端的對象是Object.prototype,再往上就是null了。因此原型鏈能夠認爲是衆多對象利用_proto串成的引用鏈,有點相似單鏈表,引用鏈的最後一個節點是Object.prototype。

 

(四) 維護constructor

  只有當咱們建立一個函數(JS中的函數也是對象)時,會自動爲這個函數附上prototype對象, prototype中的全部屬性會被遺傳到該函數建立的對象上。在prototype的屬性中,比較特殊的是constructor,constructor的值就是這個函數自己。賦上prototype對象的這個過程相似於:

Js代碼   收藏代碼
  1. Foo.prototype={  
  2.     constructor:Foo  
  3. }  

 

若是咱們執行:

Js代碼   收藏代碼
  1. Foo.prototype.constructor===Foo  

 

會輸出true。一樣若是在Bar.prototype = foo語句以前執行:

Js代碼   收藏代碼
  1. Bar.prototype.constructor===Bar  

 

也會輸出true。因爲在prototype對象中的屬性會被繼承,所以foo和bar中都能訪問到constructor屬性,分別指向Foo和Bar。

 

  能夠看出,JS中的constructor比如JAVA中的Class,在JAVA中一個對象能夠經過getClass方法來獲取本身的Class,那麼JS中的對象能夠直接訪問constructor來獲取本身的構造器。在多層次的繼承問題上,咱們可能須要維護正確的繼承結構。因爲沒法訪問_proto屬性,所以繼承鏈的維護只能依靠constructor屬性。

Js代碼   收藏代碼
  1. function Foo(){}  
  2. var foo=new Foo();  
  3. function Bar(){}  
  4. Bar.prototype=foo;  
  5. var bar=new Bar();  
  6. console.log(bar.constructor) //Foo  

 

  運行上面這個例子,最後的輸出結果爲Foo。緣由是foo中的constructor指向了Foo,而bar又從foo對象繼承了該屬性。這個時候須要進行一些修改,使得bar的constructor屬性可以正確指向Bar。通常有兩種方式來處理這個問題。一是在構造器裏面修改constructor的值,第二種是在構造完成以後進行修改。

Js代碼   收藏代碼
  1. function Foo(){}  
  2. var foo=new Foo();  
  3. function Bar(){  
  4.     this.constructor=Bar;  
  5. }  
  6. Bar.prototype=foo;  
  7. var bar=new Bar();  
  8. bar.constructor //Bar   
Js代碼   收藏代碼
  1. function Foo(){}  
  2. var foo=new Foo();  
  3. function Bar(){}  
  4. Bar.prototype=foo;  
  5. var bar=new Bar();  
  6. bar.constructor= Bar;  

 

  若是使用下邊一種方式,每次實例化一個Bar的對象,都要進行修改。推薦使用上邊的方式來修改constructor屬性,此修改會對全部Bar的實例生效。

 

DOJO中的面向對象__第二章 Dojo中的類

(一) 利用DOJO.DECLARE聲明類

  在第一章中講到,JS提供的是一種基於原型(Prototype)的對象系統。這與JS誕生的初衷有關,在把JS僅僅用做頁面腳本時,利用Prototype是一種優點。由於基於原型的對象不只節約內存,初始化速度快,更重要的是還擁有了原型繼承帶來的動態特性。可是若是須要在RIA環境下構建豐富的web應用,JS的原型系統又顯得不太給力。在繼承性方面,原型系統可以讓兩個對象之間發生繼承關係,但須要通過構造器來銜接。在封裝性方面,JS的表現很糟糕,除了沒有關鍵字支持,JS實質上也只有‘全局’和‘函數’兩種級別的做用域。在多態性方面,因爲缺乏靜態類型檢查,JS對象沒法準確識別子類或者父類的同名方法,更沒法調用父類中已經定義的方法。因此歸根結底的說,JS裏採用的原型的對象系統並非完備的面向對象系統。

 

  對此,Dojo在面向對象的一些特性方面對JS進行了補充,使得開發者可以儘量的利用Dojo以一種基於類的面向對象思想來組織構建程序。Dojo中提供了一種簡單的類型聲明方式——dojo.declare,咱們能夠用來方便的對類型進行描述。以下所示,dojo.declare的格式很簡單:

Js代碼   收藏代碼
  1. dojo.declare(className,superclass, props);  

 

  其中className定義了類型的名稱。superclass指定了父類型,若是想聲明的類並不存在父類,那麼superclass能夠爲null。因爲在Dojo中支持多繼承,所以superclass能夠是一個包含了全部父類的數組。最後的props是一個對象,其中包含了該類的全部字段以及方法。在props中能夠定義一個特殊的函數constructor,constructor在該類型被實例化的時候會被自動調用到,至關於構造函數。例如:

Js代碼   收藏代碼
  1. dojo.declare('People',null,{  
  2.     name:'unknown name',  
  3.     constructor:function(name){  
  4.         this.name=name;  
  5.     }  
  6. });  
  7. var p=new People('Jack');  

(二) 定義繼承

  dojo. declare除了可以聲明類,還能對類進行擴展,進而達到繼承目的。這裏咱們只討論單繼承的例子,因爲多繼承較爲複雜,第三章中會進行詳細描述。在傳統的面相對象語言(例如JAVA)中,子類可以添加父類中沒有的字段以及方法,這也是繼承最大的意義——擴展。擴展能補充父類不具有的功能,經過多層次的繼承,逐漸構建出豐富而強大的模塊。除了擴展,子類型須要擁有從父類型中繼承字段、方法的能力,還須要屏蔽字段、覆寫方法以及調用父類型方法(包括調用父類型的構造函數)的能力,而這些能力也是大型面向對象系統的基本特性。

 

  但從原生態的JS來看,JS並不能徹底知足這些要求。咱們能夠在一個JS對象中添加本身的屬性,以此來屏蔽或者覆寫原型對象中的屬性,代價是這樣作的話就失去了訪問原型對象中的屬性的能力。說白了,JS僅僅可以依靠回溯原型鏈來訪問原型對象中的屬性,這也是受原型繼承機制的限制。

Js代碼   收藏代碼
  1. dojo.declare('People',null,{  
  2.     name:'unknown name',  
  3.     action:function(){  
  4.         //do nothing  
  5.     },  
  6.     constructor:function(name){  
  7.         this.name=name;  
  8.     }  
  9. });  
  10. dojo.declare('Student',People,{  
  11.     school:'',  
  12.     action:function(){  
  13.         //I am studing  
  14.     },  
  15.     constructor:function(name,school){  
  16.         this.school=school;  
  17.     }  
  18. });  
  19. var s=new Student('Jack','Harvard');  
  20. s.name    // Jack  
  21. s.school  // Harvard  
  22. 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方法進行了覆寫。如今已經完成了一個簡單的單繼承,下面咱們來驗證這個單繼承結構的正確性。

Js代碼   收藏代碼
  1. console.log(s instanceof Student);  //true  
  2. console.log(s instanceof People);   //true  
  3. console.log(s instanceof Object);   //true  

 

  Dojo中的繼承並不要求僅限於兩個或者多個利用Dojo. declare建立出來的類。若是是利用其餘的形式建立出來的類("raw" classes),Dojo中同樣能夠對它們進行擴展,定義他們的子類。例如dojo.declare能夠指定父類爲第一章中描述的’僞類’:

Js代碼   收藏代碼
  1. //define superclass  
  2. var Foo = function(){   
  3.     this.name = 'foo';   
  4. };  
  5. Foo.prototype.say = function(){  
  6.     console.log(this.name);  
  7. };  
  8. //define subclass  
  9. var Bar = dojo.declare(Foo, {});   
  10. var bar = new Bar();  
  11. bar.say();  // foo  

 

  上例中Bar是Foo的子類,而且繼承了Foo中的字段和方法。總的來講,dojo.declare是一種很自由的建立類的方式。

 

(三) 定義靜態域

  傳統的面嚮對象語言都直接在語言層面上支持靜態域的概念。例如JAVA,其靜態域包括靜態類型的字段以及方法,靜態類型的字段由全部的實例共享,而靜態方法能夠由類型直接調用,故其中不能訪問非靜態字段(只能先產生實例,在經過實例訪問字段)。JS並無直接支持靜態域這個概念,但能夠經過模擬的方式來達到靜態域的效果。下面一個例子展現了Dojo中如何定義靜態域:

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     staticFields: { num: 0 },  
  3.     add:function(){  
  4.         this.staticFields.num++;  
  5.     }  
  6. });  
  7. var f1=new Foo();  
  8. var f2=new Foo();  
  9. f1.add();  
  10. f2.add();  
  11. console.log(f1.staticFields.num ) //2  
 

  在這個例子中,咱們定義了一個staticFields對象。該對象用來存放全部的靜態字段以及靜態方法。最終打印結果爲2,也就是說staticFields中的變量確實由Foo的全部實例所共享。

 

  事實上,staticFields並不是Dojo中聲明靜態域的一個特殊標識符。可以被全部實例共享,與staticFields這個名稱自己無關。若是咱們用其餘標識符來替代,好比 sharedVars,也會起到相同的靜態域的效果。例如:

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     sharedVars: [1,2,3,4]  
  3. });  
  4. var f1=new Foo();  
  5. var f2=new Foo();  
  6. f1.sharedVars.push(5);  
  7. console.log(f2.sharedVars) //[1, 2, 3, 4, 5]  
 

  這個例子中的sharedVars是一個數組。當f1對這個數組進行修改之後,一樣會反映到f2。總結一下能夠得出,dojo.declare中直接聲明的引用類型將會被全部實例共享。若是咱們在使用dojo.declare的時候不注意這點,頗有可能已經埋下了安全隱患。

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     shared: [1,2,3,4],  
  3.     constructor:function(){  
  4.         this.unshared={ num:1 };  
  5.     }  
  6. });  
  7. var f1=new Foo();  
  8. var f2=new Foo();  
  9. f1.shared.push(5);  
  10. f1.unshared.num++;  
  11. console.log(f2.shared)    //[1, 2, 3, 4, 5]  
  12. 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,還能夠不自動調用父類構造函數,而採用手動調用,這些在第三章中會有具體描述。

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     constructor:function(){ console.log('foo')  }  
  3. });  
  4. dojo.declare("Bar", Foo, {  
  5.     constructor:function(){ console.log('bar')  }  
  6. });  
  7. var b=new Bar; // 自動調用,打印foo bar  

 

  繼承並不老是對父類中的方法進行覆寫,不少時候咱們須要用到父類中定義好的功能。所以dojo提供了調用父類中非constructor方法的能力,這也是對JS缺陷的一種彌補。具體的調用採用this.inherited方式。先看一個調用父類同名方法的例子:

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     setPro:function(name){  
  3.         this.name=name;  
  4.     }  
  5. });  
  6. dojo.declare("Bar", Foo, {  
  7.     setPro:function(name,age){  
  8.         this.inherited(arguments); // 調用父類中的同名方法  
  9.         this.age=age;  
  10.     }  
  11. });  
  12. var bar=new Bar;  
  13. bar.setPro('bar',25);  

 

  this.inherited能夠在父類中尋找同名方法(這裏是set),若是找到父類中的同名方法則執行,若是沒有找到,繼續在父類的父類中尋找。注意,this.inherited能夠接受三種參數:

  • methodName(可選)調用的方法名稱,大多數時候這個參數是能夠省略的。
  • arguments(必選)經過傳遞arguments使得父類中的方法能夠獲取參數。
  • argsArray(可選)一個數組,其中能夠包含準備傳遞給父類方法的若干參數。若是存在該參數,那麼父類方法中獲取的將是這個參數,而不是arguments中的內容。

上面例子中的this.inherited(arguments) 能夠改寫成:

Js代碼   收藏代碼
  1. ……  
  2. this.inherited('setPro',arguments);  
  3. ……  

 

這種寫法明確指定了準備調用的父類方法的名稱。可是若是改寫成:

Js代碼   收藏代碼
  1. ……  
  2. this.inherited('setPro',arguments,[ 'foo']);  
  3. ……  
  4. // bar = {name:'foo',age:25}  
 

那麼執行的結果是bar.name等於foo。這些都跟inherited的具體實現有關,深刻到inherited函數,能夠發現inherited大致上執行的是以下語句:

Js代碼   收藏代碼
  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. }  

 

  其中f就是父類中的方法,args是字面量arguments,a是另外傳入的參數數組(argsArray)。若是額外傳入的參數是true,那麼直接返回f。當argsArray存在的狀況下,將執行f.apply(this, a),不然執行f.apply(this, args)。除了this.inherited,還有一個相似的函數this.getInherited。這個函數僅僅獲取指定的父類中的方法,可是並不執行。

Js代碼   收藏代碼
  1. dojo.declare("Foo", null, {  
  2.     m1:function(){  
  3.         console.log('foo');  
  4.     }  
  5. });  
  6. dojo.declare("Bar", Foo, {  
  7.     m2:function(){  
  8.         // get method m1 from Foo  
  9.         var temp=this.getInherited('m1',arguments);  
  10.         temp.apply(this);  
  11.     }  
  12. });  
  13. var bar=new Bar;  

 

若是對inherited或getInherited的實現細節感興趣,能夠參考第四章。

 

(五) 定義擴展(extend)

  dojo.declare提供了一些函數,這些函數能夠被很方便的調用,好比上面已經提到的isInstanceOf函數,inherited函數,getInherited函數。這些函數都是做用在某個類型的實例或者說對象上。此外,還提供了一個用於mixin的函數——extend()。與上面幾個函數不一樣,extend直接被dojo.declare定義的類所調用。


  extend最多見的用法是對類型進行擴展,增長原先沒有的新屬性(方法)。固然也能夠用它來添加劇名的屬性,不過這樣會有必定的風險替換掉原先已經定義的屬性。

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     func1:function(){ console.log('fun1')}  
  3. });  
  4. A.extend({  
  5.     func1:function(){ console.log('fun2')},  
  6.     func2:function(){ console.log('fun3')}  
  7. });  
  8. var a=new A;  
  9. a.func1();      //fun2  
  10. a.func2();      //fun3  

 

  擁有了extend能力的類型與傳統靜態語言中的類型很不同。在通常的靜態語言中,咱們沒法對一個現有的類型做出更改,這個時候若是須要對一個類型進行擴展,無非是選擇繼承、組合。而在Dojo中,即便已經完成了一個類型定義,若是未來有須要,咱們依然能夠隨時使用extend對這個類型做出改變(替換或添加屬性)。這種改變現有類型的能力,是JS帶給咱們的一種動態特性,Dojo只是對此做出了進一步封裝。

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     constructor:function(){  
  3.         console.log('1')  
  4.     }  
  5. });  
  6. A.extend({  
  7.     constructor:function(){  
  8.         console.log('2')  
  9.     }  
  10. });  
  11. var a=new A;        //'1'  

 

  注意extend沒法替換掉類定義中的構造函數。從本質上講,extend與mixin沒有任何區別。declare中的extend源碼以下:

Js代碼   收藏代碼
  1. function extend(source){  
  2.     safeMixin(this.prototype, source);  
  3.     return this;  
  4. }  

 

  這裏的this是指調用extend的類型。safeMixin會將source對象的屬性、方法(構造函數除外)毫無保留的添加到this.prototype對象中。因爲發生更改的是prototype的對象,所以extend以後,該類型的每一個實例都會擁有新賦予的能力,無論該實例是在extend以前仍是以後生成。


 

(一) 定義多繼承

  Dojo在基於類的面向對象系統方面加強了JS的表現力,在第二章中已經提到Dojo還容許用戶使用多繼承,本章將主要探討關於多繼承的內容。利用dojo.declare聲明多繼承的類是很方便的,用戶只須要傳遞一個數組(superclass )進去,superclass數組包含了全部的父類。

Js代碼   收藏代碼
  1. dojo.declare("A", null, {  
  2.     constructor: function() { console.log ("A"); }  
  3. });  
  4. dojo.declare("B", null, {  
  5.     constructor: function() { console.log("B"); },  
  6.     text: "text B"  
  7. });  
  8. dojo.declare("C", null, {  
  9.     getText: function(){ return "text C" }  
  10. });  
  11. dojo.declare("D",[A,B,C],{  
  12.     constructor: function() {   
  13.         console.log(this.text + " and " + this.getText());   
  14.     }  
  15. });  
  16. var d = new D();  
  17. // A  
  18. // B  
  19. // 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運算符並不能很好的判斷類型。拿上例來講:

Js代碼   收藏代碼
  1. instanceof A  //true  
  2. instanceof B  //false  
  3. instanceof C  //false  
  4. instanceof D  //true  
 

  很顯然,這不是用戶想要的結果。爲了應付在多繼承環境下的類型判斷,Dojo提供了相似的函數——isInstanceOf,方便用戶進行類型判斷。

Js代碼   收藏代碼
  1. d.isInstanceOf(A)       //true  
  2. d.isInstanceOf(B)       //true  
  3. d.isInstanceOf(C)       //true  
  4. d.isInstanceOf(D)       //true  

 

(二) MRO與C3算法

   面嚮對象語言若是支持了多繼承的話,都會遇到著名的菱形問題(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中能夠描述爲:

Python代碼   收藏代碼
  1. >>> O = object  
  2. >>> class A(O): pass  
  3. >>> class B(O): pass  
  4. >>> class C(A,B): pass  
 

  對C進行C3算法,獲得的結果表示爲L(C)=CABO.這個結果看起來很像是廣度優先搜索的結果,事實上它們之間是有點相似,但不老是相同。獲得的線性化序列CABO保證了Python在調用方法的時候,C是第一個被搜索的,A老是優先於B被先搜索到。

寫道
定義: 
C(B1 ... BN) :C類的父類是B1…BN類
L(C(A,B)) :C以及C的全部父類的線性化排列
merge(A,B) :A和B的合併
CABO的頭部:C
CABO的尾部:ABO
C3算法: 
若是一個類沒有父類,那麼該類的線性化爲:
L(C)=C
若是一個類有一個或多個父類,那麼該類的線性化爲:
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)
merge的方法: 
1. 取出merge中參數列表的第一個元素,取第一個元素的頭部,若是該頭部不在其餘元素的尾部,則該頭部合格,在merge列表中排除它,並把它當作結果的頭部。
2. 若是該元素的頭部在其餘元素的尾部,則跳到該元素的下一個元素。取出該元素的頭部,判斷它是否合格。合格則在merge列表排除它並放入結果中,不合格則重複該步驟。
3. 最終直到全部的類都被刪除,則merge成功,或者沒法找到合格的頭部,若是沒法找到合格的頭部,則merge失敗並報出異常。
 

  上面細述了C3算法,注意咱們在定義一個類的時候,傳入這個類的父類的順序直接決定了最後線性化結果的順序。下面來看一個複雜一些的例子。

Python代碼   收藏代碼
  1. >>> O = object  
  2. >>> class F(O): pass  
  3. >>> class E(O): pass  
  4. >>> class D(O): pass  
  5. >>> class C(D,F): pass  
  6. >>> class B(E,D): pass  
  7. >>> class A(B,C): pass  

 

這裏有四層繼承結構。咱們從上到下逐層計算線性化序列:

寫道
L[O] = O
L[E] = E+merge(L[O],O)
  = E+ merge(O,O)
  = EO
L[D] = DO
L[F] = FO
L[B] = B+merge(L(E),L(D),ED)
  = B+merge(EO,DO,ED)
  = BE+merge(O,DO,D)
  = BED+merge(O,O)
  = BEDO
L[C] = CDFO
L[A] = A+merge(L(B),L(C),BC)
  = A+merge(BEDO,CDFO,BC)
  = AB+merge(EDO,CDFO,C)
  = ABE+merge(DO,CDFO,C)
  = ABEC+merge(DO,DFO)
  = ABECD+merge(O,FO)
  = ABECDF+merge(O,O)
  = ABECDFO

  從L(A)=ABECDFO來看,最終A類對象調用方法時是按照ABECDFO的優先順序來搜索的。利用C3算法計算的時候須要注意並非全部的繼承結構最後都能導出線性化的序列。C3算法的第三步驟容許咱們失敗。假設有下面這樣的繼承結構: 

Js代碼   收藏代碼
  1. >>> O = object  
  2. >>> class A(O): pass  
  3. >>> class B(O): pass  
  4. >>> class C(A,B): pass  
  5. >>> class D(B,A): pass  
  6. >>> class E(C,D): pass  

 

對該繼承結構計算線性化序列:

Js代碼   收藏代碼
  1. L[O] = O  
  2. L[A] = AO         
  3. L[B] = BO  
  4. L[C] = CABO  
  5. L[D] = DBAO  
  6. L[E] = E+merge(L(C),L(D),CD)  
  7.        = E+merge(CABO,DBAO,CD)  
  8.       = EC+merge(ABO, DBAO,D)  
  9.       = 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

  dojo中MRO的處理方式與Python有一點點小區別。Python在構建對象的時候傳入父類列表,越靠前的類越容易被搜索到,表明着對新建的類越重要。反之,若是一個父類處在越高的繼承層次上,則越不容易被優先搜索到。dojo中的MRO大致上能夠參考上節中的描述。可是略有區別,描述以下:

寫道
Dojo中的C3算法:
若是一個類沒有父類,那麼該類的線性化爲:
L(C)=C
若是一個類有一個或多個父類,那麼該類的線性化爲:
L[C(B1 ... BN)] = C + merge(L[BN] ... L[B1]) 
//python中:
// L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN)

 

  具體的區別已經在上面的算法描述中被標識出,能夠看出,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算法能夠很快的推導出來。

寫道
L[A] = A
L[B] = BA
L[C] = C+merge(L(A),L(B),AB)
  = C+merge(A,BA, AB ) // Python中的MRO失敗了 

 

  相似,左邊的繼承關係在Dojo中也應該是失敗的。由於Dojo和Python中繼承結構的線性化大致上是左右相反的。但實際上,不管是左邊仍是右邊的繼承關係,在Dojo中都是成功的。在Dojo中,分別針對左邊和右邊的繼承進行MRO計算:

 

 

  在Dojo中左邊的繼承可以MRO成功,主要緣由是merge時傳入的參數比Python中少了父類型序列。以下所示:

寫道
L[C(B1 ... BN)] = C + merge(L[BN] ... L[B1])       //除了L(BN)…L[B1]的順序不一樣
L[C(B1 ... BN)] = C + merge(L[B1] ... L[BN], B1 ... BN )   //Python中還需傳入父類型的序列

  若是B1 ... BN之間(即父類型之間)彼此不存在繼承關係,那麼是否傳入父類型序列對merge的結果是不形成影響的。可是若是B1 ... BN之間存在了繼承關係,那麼merge的時候,B1 ... BN將會結果形成直接影響。不傳入父類型的序列,這正是Dojo中可以成功避免一些MRO失敗的緣由,也能夠說,Dojo中的MRO並不像Python中那麼嚴格。


  Dojo中的MRO計算是經過c3mro函數來進行的,傳入的參數是dojo.declare聲明時的superclass數組。若是想知道c3mro實現的細節,能夠參考第四章。

 

(四) mixin與多繼承

  JS中的原型繼承方式並不能支持多繼承,由於每一個構造器僅僅能指定一個原型對象,這是一種單繼承形式,因此在Dojo中也僅僅是儘可能去模擬多繼承,而並不是真正的多繼承。故本章標題中採用的多繼承字樣是不許確的,準確的說,在Dojo中使用的是mixin與單繼承結合的方式。只有一個類充當真正的父類,其他的類會被用於mixin。

 

  mixin是指將屬性添加到指定的對象中,這是一種很經常使用的擴展對象的手段。mixin行爲發生在兩個對象之間,源對象(source)和目標對象(target)。大致來講,mixin會遍歷source中的屬性,而且添加到target中去。,若是在target中已經存在了同名的屬性,那麼須要在mixin中進一步判斷,是否須要將這些同名屬性覆蓋。一個簡單的mixin實現以下:

Js代碼   收藏代碼
  1. function mixin(target, source){  
  2.     for(name in source){  
  3.         target[name] = source[name];  
  4.     }  
  5. }  

實際上Dojo中mixin的也相似於這樣來實現,只是加了一些判斷條件。

 

  在上一節中已經描述過Dojo中的MRO計算。在Dojo.declare進行處理的時候,首先對superclass進行MRO計算,並返回一個由構造器組成的數組。緊接着須要根據這個數組(序列),構建出原型鏈。該原型鏈中包含了全部數組中出現的構造器,包括在superclass中的和不在superclass中的。只有當這條原型鏈被構建好,關於繼承所作的工做才真正完成。在構建原型鏈的過程當中,Dojo不斷的利用mixin與匿名函數的組合,模擬出多繼承的實現。舉例來講:

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     funA:function(){}  
  3. });  
  4. dojo.declare('B',null,{  
  5.     funB:function(){}  
  6. });  
  7. dojo.declare('C',null,{  
  8.     funC:function(){}  
  9. });  
  10. dojo.declare("D",[A,B,C],{});   
  11. 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函數。例如,對於上面的例一:

Js代碼   收藏代碼
  1. var f = new F();  
  2. console.log(f instanceof A );       //false  
  3. console.log(f.isInstanceOf(A));     //true  
  4. console.log(f instanceof B );       //false  
  5. console.log(f instanceof C );       //false  
  6. console.log(f instanceof E );       //false  
  7. console.log(f instanceof F );       //true  
  8. console.log(f instanceof D );       //true  
 

  根據L(F)= FECBAD能夠看出,類型F處於繼承結構的最底端,而類型D是F的父類,處於最頂端。這兩個類型都可以直接被instanceof識別,其他的ABCE都只能利用Dojo提供的isInstanceOf才能返回true。


 

  這是dojo.declare中的三個極度蛋疼的功能,在對多繼承的實質有所瞭解以後,纔會加深對這三個功能的認識,因此放到最後說。這裏就不談它們的實現原理了,第四章中也許會描述到= =!

 

  若是以爲運行constructor先後缺乏了些什麼,那麼preamble、postscript能夠很好的幫助咱們進行彌補。根據我時間不長的開發經驗,還想不出什麼狀況下須要這種操做來彌補。若是在類型的定義中包含了preamble方法,那麼在這個類型的構造函數被調用以前,會首先執行一次preamble。一樣若是定義了postscript方法,那麼該類型的構造函數被調用以後,也會自動執行一遍postscript。下面是一個簡單的例子:

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     preamble:function(){ console.log('A'); },     
  3.     constructor:function(){ console.log('AA'); },  
  4.     postscript:function(){ console.log('AAA'); }  
  5. });  
  6. var a= new A();  
  7. /*輸出: 
  8. AA 
  9. AAA 
  10. */  
 

  至於preamble和postscript方法到底是如何被調用的,第四章中有解釋,暫時不須要關注,能夠認爲這是Dojo提供好的機制。來個複雜一些的例子:

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     preamble:function(){ console.log('A'); },     
  3.     constructor:function(){ console.log('AA'); },  
  4.     postscript:function(){ console.log('AAA'); }  
  5. });  
  6. dojo.declare('B',A,{      
  7.     preamble:function(){ console.log('B'); },     
  8.     constructor:function(){ console.log('BB'); },  
  9.     postscript:function(){ console.log('BBB'); }  
  10. });  
  11. dojo.declare('C',B,{      
  12.     preamble:function(){ console.log('C'); },     
  13.     constructor:function(){ console.log('CC'); },  
  14.     postscript:function(){ console.log('CCC'); }  
  15. });  
  16. var c= new C();  
  17. /* 輸出: 
  18. AA 
  19. BB 
  20. CC 
  21. CCC 
  22. */  
 

  從輸出的結果來看,咱們能夠挖掘出一些有意思的事情。在這種擁有繼承的狀況下,父類中postscript方法是不會被自動調用到的。上述例子的準確函數執行順序是:

寫道
1. C.preamble
2. B.preamble
3. A.preamble
4. A.constructor
5. B.constructor
6. C.constructor
7. C.postscript
 

  至於爲何不會調用到A和B的postscript方法,從Dojo的源碼實現上講是由於這裏所調用的父類型的constructor並無去執行postscript方法。換個角度說,這裏調用父類型的constructor函數完成的構造過程,與咱們直接經過new來調用的父類型發生的構造,是兩回事。概括來講,對類型L(A)= AA1A2A3…AN使用new進行實例化時,默認的執行順序是:

寫道
A.preamble-> A1.preamble-> A2.preamble ... AN.preamble-> 
AN.constructor-> AN-1.constructor ... A.constructor->
A.postscript
 

  在Dojo1.4以後的版本中,preamble已經被標記爲deprecated函數,不過postscript並無被列入deprecated。chain提供了自動執行父類中函數的功能。默認狀況下,只有父類的構造函數是會被自動調用的,而且老是先於子類的構造函數執行。只有在一些特殊狀況下,咱們會須要讓其餘的函數也可以像構造函數同樣,自動執行,免去咱們手工調用的麻煩。舉例來講,若是建立的類型包含了destroy函數,該函數會進行一些垃圾回收方面的工做,咱們確定但願destroy函數完成後也會自動去執行一下父類中的destroy。


  下面的例子定義了一條destroy函數組成的chain。其中的容許咱們來設置函數的執行順序,這裏指定的是before順序,也就是說子類的函數會先於父類的函數執行,因此子類的destroy先運行。

Js代碼   收藏代碼
  1. dojo.declare('A',null,{   
  2.     constructor:function(){ console.log('A'); },  
  3.     destroy:function(){console.log('AA');}  
  4. });  
  5. dojo.declare('B',A,{       
  6.     constructor:function(){ console.log('B'); },  
  7.     destroy:function(){console.log('BB');}  
  8. });  
  9.   
  10. dojo.declare('C',B,{      
  11.     "-chains-": {  
  12.             destroy: "before"  
  13.     },  
  14.     constructor:function(){ console.log('C'); },  
  15.     destroy:function(){console.log('CC');}  
  16. });  
  17.   
  18. var c= new C();  
  19. c.destroy();  
  20. /*輸出: 
  21. CC 
  22. BB 
  23. AA 
  24. */  
 

有兩點值得注意:


  第一點是"-chains-"語句所處的位置,上例中放在了C類型的定義中。若是放在A或者B類中,執行c.destroy()的效果仍是同樣的。事實上,只要把chain聲明放在繼承鏈條中的任何一個類型定義裏,均可以達到串連全部同名函數的效果。對於複雜的多重繼承結構也是這樣的,由於他們實質上最終仍是一條單繼承結構。


  第二點是chain中容許咱們聲明三種類型的順序,他們可以產生效果的對象不一樣。字面上,咱們可以使用的是after\before\manual這三個順序,他們分別表明了在父類函數執行以後執行、在父類函數執行以前執行、手動調用。對於非構造函數,設置manual是沒有意義的,若是不是after順序,會被一律視爲before。而對於構造函數,設置before是沒有意義的,由於父類的構造函數要麼manual手動調用,要麼必定會在子類的構造函數以前執行。

Js代碼   收藏代碼
  1. dojo.declare('A',null,{   
  2.     "-chains-": {  
  3.             constructor: "before",  //沒有做用,非‘manual’即被視爲‘after’  
  4.             foo: "manual"               //沒有做用,非‘after’即被視爲‘before’  
  5.     },  
  6.     constructor:function(){ console.log('A'); },  
  7.     foo:function(){console.log('AA');}  
  8. });  
  9. dojo.declare('B',A,{      
  10.     constructor:function(){ console.log('B'); },  
  11.     foo:function(){console.log('BB');}  
  12. });  
  13. dojo.declare('C',B,{      
  14.     constructor:function(){ console.log('C'); },  
  15.     foo:function(){console.log('CC');}  
  16. });  
  17. var c= new C();  
  18. c.destroy();  
  19. /*輸出: 
  20. CC 
  21. BB 
  22. AA 
  23. */  
 

  最後來看一個針對構造函數設置manual的例子。

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     "-chains-": { constructor: "manual" },  
  3.     constructor:function(){ console.log('A'); }  
  4. });  
  5. dojo.declare('B',A,{          
  6.     constructor:function(){ console.log('B'); }  
  7. });  
  8. dojo.declare('C',B,{      
  9.     constructor:function(){       
  10.         this.inherited(arguments); //設置爲manual後,只能手動調用父類函數  
  11.         console.log('C');   
  12.     }  
  13. });  
  14. var c= new C();  
  15. /*輸出: 
  16. */  
 

  從這個例子能夠看出,在設置了manual後,若是不手動去調用父類的構造函數,那麼父類的構造函數是不會執行的,所以這裏就不會打印A,根據第二章中的描述,手動調用能夠使用inherited方法。

 

  PS,以前我覺得preamble、postscript以及chain會在dijit中被較多使用到,但根據在Dojo1.5源碼中的搜索,很不幸,只有postscript在dijit中被使用過,至於preamble和chain基本上在整個Dojo的實現代碼中都沒有,只有在test的代碼裏出現過兩三次。可見這些功能偏門 到什麼程度。我以爲API提供的原則應該是簡單易用 ,而Dojo的接口每每體現着龐大複雜精深,我想這可能也是不少web fronter不肯意花成本去學習去使用的Dojo的緣由吧。其實做爲開發者來講,Dojo用熟了也並無感受太複雜,你甚至會爲它的細緻周全感到震撼,可是對於初學者來講,或者是那些急於上手某個Ajax框架進行開發的人,Dojo的確不是一個好的選擇。

 

  declare.js中包含了整個dojo面向對象中最重要的代碼,即對類型表達和擴展的一些封裝。功能雖然強大,可是幸虧文件並不複雜,擁有清晰的脈絡。整個declare.js一共定義了15個函數,14個具名函數,1個匿名函數。這14個具名函數中又有一些是declare.js內部使用的函數,外部沒法調用,還有一些是由dojo提供的接口,能夠供dojo.declare聲明的類型來調用。具體函數以下所示:

Js代碼   收藏代碼
  1. //declare.js的結構(來源於Dojo1.5版):  
  2. (function(){  
  3.     //9個內部函數,外部沒法調用   
  4.     function err(){...}  
  5.     function c3mro(){...}  
  6.     function mixOwn(){...}  
  7.     function chainedConstructor(){...}  
  8.     function singleConstructor(){...}  
  9.     function simpleConstructor(){...}  
  10.     function chain(){...}  
  11.     function forceNew(){...}  
  12.     function applyNew(){...}  
  13.     //5個對外提供的接口  
  14.     function inherited(){...}  
  15.     function getInherited(){...}  
  16.     function isInstanceOf(){...}  
  17.     function extend()   {...}  
  18.     function safeMixin(){...}  
  19.     //1個匿名函數:Dojo.declare  
  20.     d.declare = function(){...}  
  21. })();  

 

  其中最爲核心的即匿名函數,用戶在使用dojo.declare的時候便是使用的該函數。該函數負責產生一個實實在在的能夠new出實例的JS構造器。本章打算從該函數開始,逐漸分析整個declare.js文件。

(一)    dojo.declare

1)    crack  parameters

  先來看declare的參數列表以及函數的開頭部分。其中的d在整個declare.js的開始即被賦值爲dojo,至於傳入的三個參數className,superclass,props曾經在第二章中有過解釋。其中className表明要聲明的類型的名稱,superclass指定了父類型列表,最後的props對象包含了類型的全部構造函數、字段、方法。

Js代碼   收藏代碼
  1. d.declare = function(className, superclass, props){  
  2.         // crack parameters  
  3.         if(typeof className != "string"){  
  4.             props = superclass;  
  5.             superclass = className;  
  6.             className = "";  
  7.         }  
  8.         props = props || {};  
  9.         ……  
  10. }  
 

  因爲dojo.declare支持聲明一個匿名類型,所以參數列表中的第一個參數className能夠省略,因此纔有了crack parameters 這一步驟。其實最後一個props參數也能夠省略,若是這樣dojo會自動用一個空的對象代替。

Js代碼   收藏代碼
  1. //正常的類型聲明  
  2. dojo.declare('A',null,null);  
  3. //匿名類,繼承自類型A  
  4. var B = dojo.declare(A);  

 2)    計算MRO、指定父類型

  在處理完傳入的參數以後,緊接着的一步就是計算MRO繼承序列。固然,前提是用戶在使用dojo.declare聲明的時候傳入了superclass。以前一直superclass說是一個包含了多個類型的數組,其實superclass也能夠不是數組類型,在單繼承的時候,superclass就是惟一的一個父類型,另外在沒有繼承的時候,superclass必定要寫成null。
  因此在代碼的實現中,首先判斷了superclass是否是一個數組。這裏判斷數組的時候利用了 Object.prototype.toString函數(這裏的opts),去年我在求職的時候很多公司都問到了這個問題:JS中如何檢測數組類型,惋惜當時沒有好好研究過,因此都不能算答上。
  若是這裏肯定了superclass是一個數組,那麼則調用c3mro函數來計算MRO。注意這裏返回的MRO數組中第一個保存的並不是某類型自己,而是一個偏移量,表示實際上的惟一一個父類在MRO結果中距離頂端的偏移量。有了這個偏移量,天然很方便的能夠從MRO結果中獲取父類型。

  若是superclass不是一個數組,而是一個構造器,那麼確定這是一個單繼承的狀況。這裏的判斷也頗有意思,一樣是利用的Object.prototype.toString函數,若是返回的結果爲object Function,那麼superclass確定是一個函數(構造器)。接下來一樣是構造存放MRO的bases數組。

Js代碼   收藏代碼
  1. // 若是傳入的superclass是一個數組,那麼調用c3mro來計算MRO  
  2. // 計算出來的結果放在bases數組中  
  3. if(opts.call(superclass) == "[object Array]"){  
  4.     // bases中保存了根據superclass計算出來的MRO      
  5.     bases = c3mro(superclass);  
  6.     // 其中bases[0]保存了真正的那個父類在MRO序列中距離頂端的距離  
  7.     t = bases[0];  
  8.     mixins = bases.length - t;  
  9.     superclass = bases[mixins];  
  10. }   
  11. // 若是傳入的superclass是否是數組,那麼判斷它是構造器仍是null  
  12. // 既不是構造器也不是null則會拋出異常  
  13. else{  
  14.     bases = [0];  
  15.     if(superclass){  
  16.         // 若是傳入的superclass是一個構造器  
  17.         if(opts.call(superclass) == "[object Function]"){  
  18.             // 判斷superclass是raw class仍是Dojo中的類  
  19.             t = superclass._meta;  
  20.             // 加入superclass的bases  
  21.             bases = bases.concat(t ? t.bases : superclass);  
  22.         }else{  
  23.             err("base class is not a callable constructor.");  
  24.         }  
  25.     }else if(superclass !== null){  
  26.         err("unknown base class. Did you use dojo.require to pull it in?")  
  27.     }  
  28. }  
 

  這裏還有一個值得注意的地方是,superclass既多是一個原生態的JS構造器,也多是利用dojo.declare聲明出來的構造器。若是是Dojo中的構造器,那麼能夠直接獲取superclass._meta.bases,這裏記載了superclass的MRO序列。若是僅僅是JS原生的構造器,那麼會將superclass自己加入bases。

3)    構造單繼承鏈

  有了MRO序列以後就應該構造一條單繼承結構,整個過程大概相似於從MRO中的父類開始,逐漸向前取出MRO中的類型,mixin進一條繼承鏈。這個過程以前已經用過一個實例來表示出來,如第三章第四小節中的圖所示。

Js代碼   收藏代碼
  1. // 構造完整的繼承鏈  
  2. if(superclass){  
  3.     // 從bases中的父類型開始向前掃描,直到i=0時爲止  
  4.     for(i = mixins - 1;; --i){  
  5.         // superclass是目前已經處理到的類型  
  6.         // proto是從superclass.prototype繼承來的一個對象  
  7.         proto = forceNew(superclass);  
  8.         if(!i){  
  9.             break;  
  10.         }  
  11.         // t是bases中位於superclass前的類型  
  12.         t = bases[i];  
  13.         // 在proto中mixin進t.prototype中的屬性  
  14.         (t._meta ? mixOwn : mix)(proto, t.prototype);  
  15.         // 建立新的superclass,其prototype被設爲proto  
  16.         // 等於該superclass類型繼承自原先的superclass  
  17.         ctor = new Function;  
  18.         ctor.superclass = superclass;  
  19.         ctor.prototype = proto;  
  20.         superclass = proto.constructor = ctor;  
  21.     }  
  22. }  
  23. else{  
  24.     proto = {};  
  25. }  
  26. // 這裏的props是dojo.declare時傳入的參數  
  27. // safeMixin會將props中除了constructor的屬性都mixin進proto中  
  28. // 因爲proto最後充當了ctor.prototype,所以這些屬性都會被當作static屬性  
  29. // 被全部ctor的實例共享,ctor即爲dojo.declare最終產生的類型。  
  30. safeMixin(proto, props);  
  31. // 若是在props中有自定義的constructor,則賦給proto.constructor  
  32. t = props.constructor;  
  33. if(t !== op.constructor){  
  34.     t.nom = cname;  
  35.     proto.constructor = t;  
  36. }  
 

  這裏代碼中的for循環會從父類型的前一個元素開始依次遍歷,mixins表明了父類型在bases(MRO的計算結果)中的位置,整個循環的結束會在i=0時結束。3.4章節的這個例子中,父類型位於bases的最末尾,在繼承鏈中也是出於最高點,因此for循環也完整的遍歷了一次bases數組。可是並不是全部的繼承結構都會致使完整的遍歷bases數組,既然是在bases中從父類型開始向前遍歷,那麼只要父類型若是不處於bases的末尾,就無需整個遍歷bases了。舉例來講:

Js代碼   收藏代碼
  1. dojo.declare('A',null,{});  
  2. dojo.declare('B',A,{});  
  3. dojo.declare('C',B,{});  
  4. dojo.declare("D",[C,B,A],{});  
 

  對於這樣一個繼承結構,理論上計算出來的D類的MRO爲L(D)= DCBA。實際上經過c3mro方法返回的bases數組是[3,C,B,A],那麼指定出的父類型就是C(右起第3個)。當superclass爲C的時候,mixins=1,i=0,這裏的for循環就不會再處理到B和A,這時候僅僅執行了一條proto = forceNew(superclass)語句就跳出了循環。這樣的處理也是有道理的,由於C其實已經繼承自B和A,也就是說如D僅僅須要繼承了C,就會自動繼承B和A中的屬性,因此再處理B和A是沒有意義的。

4)    處理chains

  這一部分的內容相對簡單,代碼也很容易理解,但有一些邏輯上的細節須要注意一下。

Js代碼   收藏代碼
  1. // 依然是從bases中的superclass前一個位置開始  
  2. // 依次將各個類型中的chains定義加入到chains中  
  3. for(i = mixins - 1; i; --i){   
  4.     t = bases[i]._meta;  
  5.     if(t && t.chains){  
  6.         chains = mix(chains || {}, t.chains);  
  7.     }  
  8. }  
  9. // 將proto中定義的chains加入chains  
  10. if(proto["-chains-"]){  
  11.     chains = mix(chains || {}, proto["-chains-"]);  
  12. }  
 

  這裏chains是declare.js開頭就定義好的一個對象,專門用來存儲一個類型中的chains。代碼中首先會從i = mixins - 1開始遍歷,逐步將bases中類的chains加入到這裏的chains來。在for循環結束以後,會最後再加入proto裏的chains。

  代碼中彷佛是少了對superclass類型的處理,是的。。。。。的確沒處理。。。。。這會形成一個詭異的問題,依然是前面出現過的繼承結構:

Js代碼   收藏代碼
  1. dojo.declare('A',null,{  
  2.     "-chains-": {bar: "after"},  
  3.     foo:function(){console.log('A')},  
  4.     bar:function(){console.log('A')}  
  5. });  
  6. dojo.declare('B',null,{});  
  7. dojo.declare('C',null,{});  
  8. dojo.declare("D",[A,B,C],{  
  9.     "-chains-": {foo: "before"},  
  10.     foo:function(){console.log('D')},  
  11.     bar:function(){console.log('D')}  
  12. });   
  13. var d = new D();  
  14. d.foo();  
  15. d.bar();  
 

  根據上面代碼的意思,咱們預期在執行bar函數的時候,會輸出A、D。可是實際上,在執行bar的時候,僅僅會輸出一個D。這是由於A中定義的chains失效了,理由其實很簡單,由於在處理chains的時候for循環中並不處理A,由於A是D的真正父類。而後再處理proto["-chains-"]的時候,又沒有處理到A,由於D自帶了一個proto["-chains-"],覆蓋掉了A中的chains。這就形成了A中定義chains失效了的局面。所以最正確的定義chains的方式是隻在一個類型中定義,好比上例統一在D中進行定義,只有這樣才能夠避免上述狀況。

5)    建立ctor

  這邊的ctor就是最後dojo.declare定義出來的類型,其本質上是JS中的一個構造器。

Js代碼   收藏代碼
  1. t = !chains || !chains.hasOwnProperty(cname);  
  2. bases[0] = ctor =   
  3.     (chains&&chains.constructor==="manual") ?   simpleConstructor(bases) :  
  4.     (  
  5.         bases.length == 1 ? singleConstructor(props.constructor, t) :chainedConstructor(bases, t)  
  6.     );  
 

  這裏的t是一個flag,若是在以前的chains中已經包含了constructor 的設置,則爲false,別的狀況下都是true。下面則是分狀況調用三個不一樣的函數來構造ctor:

  • 若是chains中設置了constructor==="manual",則調用simpleConstructor
  • 若是bases.length == 1即沒有父類,談不上繼承,調用singleConstructor
  • 最後若是不是上面兩種狀況,調用chainedConstructor

  若是沒有chains,這邊的代碼將美妙許多,既不須要爲了構造chains去遍歷bases,也不用調用chainedConstructor這樣使人費解的函數。這邊暫且無論上述三個鑄成ctor的函數。接下來的代碼就已經很容易理解了,雖然不短,可是都是常規的一些作法。具體解釋以下:

Js代碼   收藏代碼
  1. // 添加meta信息  
  2. ctor._meta  = {  
  3.     bases: bases,   
  4.     hidden: props,   
  5.     chains: chains,  
  6.     parents: parents,   
  7.     ctor: props.constructor// 這是用戶定義時寫的constructor函數  
  8. };  
  9. // 這邊的superclass是最後一個被mixin進繼承鏈的類型  
  10. ctor.superclass = superclass && superclass.prototype;  
  11. // 這邊的superclass是最後一  
  12. ctor.extend = extend;  
  13. // 將ctor的原型設置成proto  
  14. ctor.prototype = proto;  
  15. proto.constructor = ctor;  
  16.   
  17. // 在proto中添加三個函數,用於new出來的對象調用  
  18. proto.getInherited = getInherited;  
  19. proto.inherited = inherited;  
  20. proto.isInstanceOf = isInstanceOf;  
  21.   
  22. // 若是有className,那麼就不是一個匿名類型,須要調用到Dojo.setObject向  
  23. // dojo.global來註冊,在瀏覽器中就是把ctor添加在window對象中。  
  24. if(className){  
  25.     proto.declaredClass = className;  
  26.     d.setObject(className, ctor);  
  27. }  
 

  meta信息能夠用來判斷一個類型是由dojo.declare生成的仍是raw class。若是是raw class,那麼確定不會包含這些meta信息。這裏對外提供了四個接口函數能夠用供調用,一個是extend,還有三個是getInherited、inherited、isInstanceOf,以前都已經提過它們的用法,這裏再也不贅述。

6)    處理chains並返回ctor

  若是沒有chains這樣的功能,那麼剛開始的if語句中的代碼也是能夠省略的。若是chains中已經設置了一些函數的調用順序,那麼這裏是須要進一步做出處理的。具體的處理方法是將原來的函數替換成執行chain後返回的同名函數,從實現能夠看出來爲何對於非after的方法即看做爲before。

Js代碼   收藏代碼
  1. // 針對chains中除了constructor意外的函數做出處理  
  2. if(chains){  
  3.     for(name in chains){  
  4.         if(proto[name] && typeof chains[name] == "string" && name != cname){  
  5.             t = proto[name] = chain(name, bases, chains[name] === "after");  
  6.             t.nom = name;  
  7.         }  
  8.     }  
  9. }  
  10. return ctor;  
 

最後須要將ctor返回出去,由於dojo.declare容許進行匿名類型的定義,這時候不會再向dojo.global註冊。

(二)   getInherited和inherited方法

  這兩個方法是對外提供的擴展接口,能夠用declare出來的類型的實例調用。它們的用途是返回父類中曾經出現的方法,或者對其進行調用。具體的用法在第二章中已經描述,這裏再也不舉例說明。

  1. getInherited方法主要用來返回父類中的方法;

  2. inherited方法主要用來調用父類中的方法;

  首先來看getInherited方法,該方法的實現比較簡單:

Js代碼   收藏代碼
  1. //getInherited的實現,調用了inherited  
  2. function getInherited(name, args){  
  3.     //若是傳入name,則返回指定name的方法  
  4.     if(typeof name == "string"){  
  5.         return this.inherited(name, args, true);  
  6.     }  
  7.     //不然返回的是父類中的同名方法  
  8.     return this.inherited(name, true);  
  9. }  

  上面的實現中能夠看出,在getInherited中其實仍是調用inherited來作進一步處理,只不過將inherited的最後一個參數設置爲true,在inherited裏會判斷該參數,爲true就是返回函數,不爲true則是調用函數。

 

  接下來看inherited方法,能夠分紅個3步驟依次來看inherited具體的實現細節。第一步 依然是crack arguments,這是因爲dojo提供的API的靈活性所致,就要求這樣也能調用,那樣也能調用,很蛋疼。第二步 就是根據name參數來尋找父類中的同名方法,這裏的name有多是constructor,也就是說咱們能夠利用inherited去調用父類中的構造函數,所以在inherited的實現裏作了分狀況討論,一種狀況是調用父類的非構造函數,還有一種是調用構造函數。想一想在第三章末尾曾講述過,能夠把某繼承鏈的構造器調用順序設置爲manual,這樣將會破壞默認的自動調用父類構造函數,用戶能夠根據本身手動去調用父類的構造函數,用的正是this.inherited('constructor',arguments)....在第二步完成以後,第三步 所作的工做很簡單,即決定是返回找到的同名方法仍是調用這個同名方法。

Js代碼   收藏代碼
  1. function inherited(args, a, f){  
  2.     var name, chains, bases, caller, meta, base, proto, opf, pos,  
  3.         cache = this._inherited = this._inherited || {};      
  4.     // 首先是crack arguments  
  5.     // 最後傳入的參數f多是true,也多是一個替代args的數組,還有多是默認的undefined  
  6.     if(typeof args == "string"){  
  7.         name = args;  
  8.         args = a;  
  9.         a = f;  
  10.     }  
  11.     f = 0;  
  12.   
  13.     //args是子類方法的參數列表,args.callee表明在子類的哪一個方法中調用了inherited  
  14.     caller = args.callee;     
  15.   
  16.     //獲取欲調用的父類的方法的名稱,若是沒有傳入name參數,那麼就是調用父類中的同名方法  
  17.     name = name || caller.nom;  
  18.     if(!name){  
  19.         err("can't deduce a name to call inherited()");  
  20.     }  
  21.     //這裏獲取到的是子類型的meta信息,下面接着經過meta信息來進一步獲取子類型的MRO鏈  
  22.     meta = this.constructor._meta;  
  23.     bases = meta.bases;  
  24.   
  25.     //第一次調用inherited的時候,因爲缺乏this._inherited信息,  
  26.     //因此cache是一個空的Object,這裏pos是undefined  
  27.     //可是若是第二回及之後用到了inherited  
  28.     //那麼在cache中記錄了以前一次利用inherited尋找的方法和位置  
  29.     //注意實際上整個實現中並未利用cache,這裏的cache疑似某個實現版本遺留下的痕跡  
  30.     pos = cache.p;  
  31.   
  32.     //分狀況討論  
  33.     //1.要調用的方法不是父類中的構造函數  
  34.     if(name != cname){  
  35.         // method  
  36.         if(cache.c !== caller){  
  37.             // cache bust  
  38.             pos = 0;  
  39.             base = bases[0];  
  40.             meta = base._meta;  
  41.             if(meta.hidden[name] !== caller){  
  42.                 // error detection  
  43.                 chains = meta.chains;  
  44.                 if(chains && typeof chains[name] == "string"){  
  45.                     err("calling chained method with inherited: " + name);  
  46.                 }  
  47.                 // find caller  
  48.                 do{  
  49.                     meta = base._meta;  
  50.                     proto = base.prototype;  
  51.                     if(meta && (proto[name] === caller && proto.hasOwnProperty(name) || meta.hidden[name] === caller)){  
  52.                         break;  
  53.                     }  
  54.                 }while(base = bases[++pos]); // intentional assignment  
  55.                 pos = base ? pos : -1;  
  56.             }  
  57.         }  
  58.         // 在正常狀況下,在bases[0]中根據name尋找到的方法就是caller  
  59.         // 所以須要沿着bases繼續尋找,有可能會進入while循環,找到的函數放在f中            
  60.         base = bases[++pos];  
  61.         if(base){  
  62.             proto = base.prototype;  
  63.             if(base._meta && proto.hasOwnProperty(name)){  
  64.                 f = proto[name];  
  65.             }else{  
  66.                 opf = op[name];  
  67.                 do{  
  68.                     proto = base.prototype;  
  69.                     f = proto[name];  
  70.                     if(f && (base._meta ? proto.hasOwnProperty(name) : f !== opf)){  
  71.                         break;  
  72.                     }  
  73.                 }while(base = bases[++pos]); // intentional assignment  
  74.             }  
  75.         }  
  76.         //這個寫法過高級了....就是在bases中沒有去就Object.prototype裏找  
  77.         //不過極可能Object.prototype中依然沒有名爲name的方法,這時候f就是undefined  
  78.         f = base && f || op[name];  
  79.     }  
  80.     //2.要調用的方法是父類中的構造函數  
  81.     else{  
  82.         if(cache.c !== caller){  
  83.             //若是name是constructor,依然是沿着bases依次尋找  
  84.             pos = 0;  
  85.             meta = bases[0]._meta;  
  86.             if(meta && meta.ctor !== caller){  
  87.                 // error detection  
  88.                 chains = meta.chains;  
  89.                 if(!chains || chains.constructor !== "manual"){  
  90.                     err("calling chained constructor with inherited");  
  91.                 }  
  92.                 // find caller  
  93.                 while(base = bases[++pos]){ // intentional assignment  
  94.                     meta = base._meta;  
  95.                     if(meta && meta.ctor === caller){  
  96.                         break;  
  97.                     }  
  98.                 }  
  99.                 pos = base ? pos : -1;  
  100.             }  
  101.         }  
  102.         // 這裏找到的父類型的構造函數是base._meta.ctor  
  103.         // 即當初declare該類型時props中自定義的constructor函數  
  104.         while(base = bases[++pos]){ // intentional assignment  
  105.             meta = base._meta;  
  106.             f = meta ? meta.ctor : base;  
  107.             if(f){  
  108.                 break;  
  109.             }  
  110.         }  
  111.         f = base && f;  
  112.     }  
  113.   
  114.     // 把f和pos放進cache中,即this._inherited  
  115.     // 這樣下次再次利用inherited尋找name方法的時候就很方便了  
  116.     cache.c = f;  
  117.     cache.p = pos;  
  118.   
  119.     // 決定是返回f仍是調用f  
  120.     if(f){  
  121.         return a === true ? f : f.apply(this, a || args);  
  122.     }  
  123. }  

  上面不只貼出了整個inherited的實現,也標註了一些關鍵步驟的解釋。其中的cache疑似是一個過時的實現,由於實在想不出會有什麼狀況下cache.c === caller,除非人爲的故意設置成如此,不過幸虧即便忽略掉cache的做用也不會影響整段代碼的理解。還有一個頗爲有趣的地方在於若是發現了一個方法處於chains之中,那麼會拋出異常,由於對一個已經chains的方法再去手動調用是毫無心義的。

(三)  isInstanceOf方法

  在第二章中已經說起,爲了彌補JS自帶的instanceof運算符沒法判斷Dojo中的繼承,因此纔有了isInstanceOf擴展。該方法由Dojo中類型的實例來調用。

Js代碼   收藏代碼
  1. function isInstanceOf(cls){  
  2.     //獲取該類型meta信息中的bases(即MRO的結果)  
  3.     var bases = this.constructor._meta.bases;  
  4.     //遍歷bases  
  5.     for(var i = 0, l = bases.length; i < l; ++i){  
  6.         if(bases[i] === cls){  
  7.             return true;  
  8.         }  
  9.     }  
  10.     return this instanceof cls;  
  11. }  

  整個實現也很清晰很簡單,在第三章曾經描述過,不論Dojo中一個繼承結構多麼的複雜,歸根結底仍是一個單繼承的形式,外加mixin進了許多類型的屬性而已。那麼在判斷instanceof的時候,只要順着這樣一條繼承鏈從低向高處遍歷,沿途不管是發現了mixin的class,或者直接就是父類,這裏都會返回true。

 

若是對JS中原生instanceof的判斷機制感興趣,能夠參考

1. http://www.iteye.com/topic/461096

2. http://ejohn.org/blog/objectgetprototypeof

 

(四)  chainedConstructor方法

  第四章中提過,在declare中構建ctor的時候,針對不一樣的狀況分別調用了三個函數。這裏只研究其中的chainedConstructor,由於該函數最爲複雜,另外兩個simpleConstructor和singleConstructor函數不做詳細分析。chainedConstructor是當declare的類型存在繼承,且未設置constructor="manual"時調用的函數,返回值是一個用以充當declare類型的函數。

 

  整個chainedConstructor能夠看做三大步。第一步 就是就是執行在繼承結構鏈上全部類型中定義的preamble函數,第二步 是調用全部類型的constructor函數,第三步 是執行當前類型中定義的postscript方法。若是撇開preamble和postscript,那麼整個chainedConstructor的實現就只須要調用全部類型的constructor函數,相應的實現也能夠被壓縮成:

Js代碼   收藏代碼
  1. // 遍歷bases中的類型,未指明constructor的調用順序,所以默認是after,即  
  2. // 從bases[i]---->bases[0]依次調用constructor  
  3. for(i = bases.length - 1; i >= 0; --i){  
  4.     f = bases[i];  
  5.     m = f._meta;  
  6.     f = m ? m.ctor : f;//多是raw class  
  7.     if(f){  
  8.         f.apply(this, arguments);  
  9.     }  
  10. }  

 

  惋惜必需要面對存在preamble和postscript的狀況,並且這兩種狀況的處理方式還不同。其實preamble的調用也很簡單,就是遍歷bases,找出其中定義過的preamble函數,而後依次執行。

Js代碼   收藏代碼
  1. function chainedConstructor(bases, ctorSpecial){  
  2.     return function(){  
  3.         var a = arguments, args = a, a0 = a[0], f, i, m,l = bases.length, preArgs;  
  4.   
  5.         //若是不是利用new的時候調用該函數,那要強迫new出實例  
  6.         if(!(this instanceof a.callee)){  
  7.             // not called via new, so force it  
  8.             return applyNew(a);  
  9.         }  
  10.   
  11.         // ctorSpecial=true僅發生在:沒有定義chains,或者chains中的constructor  
  12.         // 一旦有了chains而且設置了constructor的順序,則無需執行preamble  
  13.         // 有兩種設置preamble的方式:  
  14.         // 一是在new的時候將preamble做爲參數傳遞給該類型  
  15.         // 二是在declare一個類型的時候定義好  
  16.         if(ctorSpecial && (a0 && a0.preamble || this.preamble)){  
  17.             preArgs = new Array(bases.length);  
  18.             preArgs[0] = a;  
  19.             for(i = 0;;){  
  20.                 // 若是是在參數中定義了preamble並傳第給ctor  
  21.                 a0 = a[0];  
  22.                 if(a0){  
  23.                     f = a0.preamble;  
  24.                     if(f){  
  25.                         a = f.apply(this, a) || a;  
  26.                     }  
  27.                 }  
  28.                 // 若是是在類型的declare中定義preamble  
  29.                 f = bases[i].prototype;  
  30.                 f = f.hasOwnProperty("preamble") && f.preamble;  
  31.                 if(f){  
  32.                     a = f.apply(this, a) || a;  
  33.                 }  
  34.                 // for循環再遍歷完了bases以後會結束  
  35.                 if(++i == l){  
  36.                     break;  
  37.                 }  
  38.                 //會記錄下每次調用preamble以後的arguments  
  39.                 //主要是爲了防止某個preamble對arguments做出修改  
  40.                 preArgs[i] = a;  
  41.             }  
  42.         }  
  43.           
  44.         //第二步開始  
  45.         ……  
  46.   
  47. }  

  上面對第一步preamble的執行做出了一些解釋。如今還剩最後一步,只針對當前的類型來調用postscript:

Js代碼   收藏代碼
  1. f = this.postscript;  
  2. if(f){  
  3.      f.apply(this, args);  
  4. }  

  這個實在沒什麼好講的。

 

  從整個chainedConstructor的三大步驟實現來看,其實dojo的源碼寫的仍是很通俗易懂的,結構也很清楚,是不錯的學習材料^_^至此declare.js中比較重要的函數基本都已經講完了,只缺乏一個關於c3mro函數的剖析,可是前面講mro已經花了大量的篇幅,便不打算再寫下去了。之前都是僅僅停留在參閱Dojo的API的說明上,這是我第一次花力氣去閱讀Dojo的源碼,惋惜目前的工做中已經沒有機會再使用Dojo了。

相關文章
相關標籤/搜索