《悟透JavaScript》之甘露模型(新)

注意:如果您尚未閱讀過原來那篇老文章《悟透JavaScript》,請先行閱讀該文,以瞭解上下文關係。

在上面的示例中,我們定義了兩個語法甘露,一個是Class()函數,一個是New()函數。使用Class()甘露,我們已經可以用非常優雅的格式定義一個類。例如前例中的:

    var Employee = Class(Person,    //派生至Person類
    {
        Create: 
function(name, age, salary)
        
{
            Person.Create.call(
this, name, age);  //調用基類的構造函數
            this.salary = salary;
        }
,
        ShowMeTheMoney: 
function()
        
{
            alert(
this.name + " $" + this.salary);
        }

    }
);


    這種類的寫法已經和C#或Java的格式非常相似了。不過,其中調用基類的構造函數還需要用“Person.Create.call(this, name, age)”這樣的方式來表達。這需要用到基類的類名,並要用call這種特殊的方式來傳遞this指針。這和C#的base()以及Java的super()那樣的簡介調用方式比起來,還需要進一步美化。

    而New()函數的使用也不是很爽。前例中需要用“New(Employee, ["Steve Jobs", 53, 1234])”這樣的方式來創建對象,其中第一個參數是類,其他構造參數需要用數組包起來。這和JavaScript本來那種自然的“new Employee("Steve Jobs", 53, 1234)”比起來,醜陋多了。這也需要美化。

    爲了實現這些美化工作,我們需要回顧一下new一個對象的實質。前面我們說過:
    var anObj = new aClass();
    相當於先創建一個空白對象anObj,然後將其作爲this指針調用aClass()函數。其實,這個過程中還有一個關鍵步驟就是將aClass的prototype屬性,賦值給anObj內置的prototype屬性。儘管我們無法訪問到anObj內置的prototype屬性,但它卻爲對象提供了可以調用的方法。

    由於前例中的Class()語法甘露實際上是構造了一個原型,並將這個原型掛在了相應的原型鏈上。由於它返回的是一個對象而不是函數,因此由它定義出來的Person和Employee類也都只是對象而不是函數,無法用new Person()或new Employee()這樣的方式來創建對象。要創基於一個原型來創建對象,就需要藉助New()語法甘露來中轉這個原型。

    那麼,如果我們讓Class()語法甘露返回一個函數而不是對象,不就可以用new Person()和new Employee()這種方式來創建對象了嗎?而且,我們可爲這個返回函數創建一個繼承至相關原型鏈的原型對象,並設置到該函數的prototype屬性。這樣,我們用new方式創建這個類函數的對象時,就自然地繼承了該類的原型了。

    那麼,我們讓Class()語法甘露返回什麼函數呢?因爲Class()語法甘露返回的函數是用來創建對象的,當然應該返回該類的構造函數了,正好可以是類定義參數中的Create方法啊。這樣一來,我們也無需在New()語法甘露中間接調用Create構造函數了,事實上New()語法甘露可以完全扔掉了。

    於是,我們就有了下面這個精簡甘露模型的例子:

http://www.leadzen.cn/Books/WuTouJavaScript/1/JS24.htm


<script type="text/javascript">
    
//定義類的語法甘露:Class()
    //最後一個參數是JSON表示的類定義
    //如果參數數量大於1個,則第一個參數是基類
    //第一個和最後一個之間參數,將來可表示類實現的接口
    //返回值是類,類是一個構造函數
    function Class()
    
{
        
var aDefine = arguments[arguments.length-1]; //最後一個參數是類定義
        if(!aDefine) return;
        
var aBase = arguments.length>1 ? arguments[0] : object; //解析基類
        
        
function prototype_(){}//構造prototype的臨時函數,用於掛接原型鏈
        prototype_.prototype = aBase.prototype;  //準備傳遞prototype
        var aPrototype = new prototype_();    //建立類要用的prototype
        
        
for(var member in aDefine)  //複製類定義到當前類的prototype
            if(member!="Create")    //構造函數不用複製
                aPrototype[member] = aDefine[member];

        
if(aDefine.Create)  //若有構造函數
            var aType = aDefine.Create  //類型即爲該構造函數
        else    //否則爲默認構造函數
            aType = function()
            
{
                
this.base.apply(this, arguments);
            }
;

        aType.prototype 
= aPrototype;   //設置類(構造函數)的prototype
        aType.Base = aBase;             //設置類型關係
        aType.prototype.Type = aType;   //爲本類對象擴展一個Type屬性
        return aType;   //返回構造函數作爲類
    }
;

    
//根類object定義:
    function object(){}    //定義小寫的object根類,用於實現最基礎的方法等
    object.prototype.isA = function(aType)   //判斷對象是否屬於某類型
    {
        
var self = this.Type;
        
while(self)
        
{
            
if(self == aType) return true;
            self 
= self.Base;
        }
;
        
return false;
    }
;
    
    object.prototype.base 
= function()  //調用基類構造函數
    {
        
var Caller = object.prototype.base.caller;
        Caller 
&& Caller.Base && Caller.Base.apply(this, arguments);
    }
;
    
    
//語法甘露的應用效果:    
    var Person = Class      //默認派生自object基本類
    ({
        Create: 
function(name, age)
        
{
            
this.base();
            
this.name = name;
            
this.age = age;
        }
,
        SayHello: 
function()
        
{
            alert(
"Hello, I'm " + this.name + "" + this.age + " years old.");
        }

    }
);
    
    
var Employee = Class(Person,    //派生自Person類
    {
        Create: 
function(name, age, salary)
        
{
            
this.base(name, age);  //調用基類的構造函數
            this.salary = salary;
        }
,
        ShowMeTheMoney: 
function()
        
{
            alert(
this.name + " $" + this.salary);
        }

    }
);

    
var BillGates = new Person("Bill Gates"53);
    
var SteveJobs = new Employee("Steve Jobs"531234);
    BillGates.SayHello();
    SteveJobs.SayHello();
    SteveJobs.ShowMeTheMoney();
    
    
var LittleBill = new BillGates.Type("Little Bill"6); //用BillGate的類型建LittleBill
    LittleBill.SayHello();
    
    alert(BillGates.isA(Person));       
//true
    alert(BillGates.isA(Employee));     //false
    alert(SteveJobs.isA(Person));       //true
</script>


    這個精簡甘露模型模擬出來的類更加自然和諧,而且比前面的甘露模型更加精簡。其中的Class()函數雖然很簡短,但卻是整個模型的關鍵:

    Class()函數將最後一個參數當作類定義,如果有兩個參數,則第一個參數就表示繼承的基類。如果多於兩個參數,則第一個和最後一個之間的參數都可以用作類需要實現的接口聲明,保留給將來擴展甘露模型使用吧。

    使用Class()函數來定義一個類,實際上就是爲創建對象準備了一個構造函數,而該構造函數的prototype已經初始化爲方法表,並可繼承上層類的方法表。這樣,當用new操作符創建一個該類對象時,也就很自然地將此構造函數的原型鏈傳遞給了新構造的對象。於是,就可以採用象“new Person("Bill Gates", 53)”這樣的語法來創建對象了。

    類定義中名爲Create的函數是特別對待的,因爲這就是構造函數。如果沒有定義Create構造函數,Class()函數也會創建一個默認構造函數。事實上,這個構造函數就代表了類。除此之外,我們還爲其定義了一個Base屬性,以方便追溯繼承關係。

    在本例中的根類object的原型中,我們定義了一個base方法。有了這個方法之後,在類定義的構造函數中,就可以使用“this.base()”這樣的方式來調用基類的構造函數。這種調用基類的方式和C#的base及Java的super就非常相似了。

    不過,Class()函數中還是有個小問題,那就是不支持toString()方法的覆寫。也就是說,如果我們爲一個類定義了自己的toString()方法,調用Class()函數來生成類時,toString()方法會丟失。

原來toString()方法被JavaScript規定爲不可枚舉的內置方法。除此之外還有還有toLocaleString(), valueOf(), hasOwnProperty(), isPrototypeOf(), propertyIsEnumerable()等,都是不能枚舉的內置方法。這樣在Class()函數中的那個for(…in…)語句就不能遍歷到這些屬性,導致問題的產生。

因此,這個Class()語法甘露還不能支持不可枚舉內置方法的覆寫。這不能不說是一個小小的遺憾。當然,遇到需要完全覆寫這些內置方法的情況並不多。頂多偶爾會有toString(), toLocaleString(), valueOf()這三個方法的覆寫,其他幾個幾乎不會有覆寫的情況。

除此之外,object根類的base()方法也還有個小問題。這個方法用到了函數的caller屬性,以此判斷構造函數的層次。而Opera瀏覽器不支持函數的caller屬性,因此base方法不適合於Opera瀏覽器,這不能不說是另一個更大的遺憾。

    如果在甘露模型中留有這樣的問題,想必觀音姐姐也會遺憾。我們還需要繼續努力,別讓觀音姐姐失望啊。

    其實,要解決不能覆寫非枚舉屬性的問題也並非難事。既然這些屬性是特殊的,我們就可以對其進行特殊處理。我們可以在複製完可枚舉的屬性之後,加上類似下面的特殊判斷處理語句:

        if(aDefine.toString != Object.prototype.toString)
            aPrototype.toString = aDefine.toString;

    因爲,如果覆寫了toString方法,那麼它就肯定不等於原生的toString方法,這時就可複製toString方法。當然,我們也可以不通過比較,而是用hasOwnProperty來判斷是否覆寫了toString方法:

        if(aDefine.hasOwnProperty("toString"))
            aPrototype.toString = aDefine.toString;

    使用哪種判斷方式可以任選。我們建議採用直接比較方式,這樣可以避免萬一遇到覆寫Object.prototype.hasOwnProperty的情況,也不至於出問題。當然,這也似乎有點太鑽牛角尖了。
    在實際的應用中,我們建議根據具體應用情況來決定是否需要支持特殊屬性的覆寫。如果在應用中根本不會覆寫這些特殊屬性,就無需加上這樣的特殊處理。如果是打造專業的AJAX類庫,最多支持toString(), toLocaleString(), valueOf()這三個方法的覆寫就可以了。千萬不要玩畫蛇添足的遊戲。

    最頭痛的是對base()函數的重寫,也就要兼容不支持caller屬性的Opera瀏覽器。雖然Opera瀏覽器目前只佔很小的市場範圍,但也算有名的四大瀏覽器之列。如果甘露模型不支持Opera瀏覽器,顯然無法彰顯觀音菩薩的法力,也更不好意思說自己是觀音老師的弟子。

    其實,base()方法之所以要使用自身的caller屬性,就是爲了確定當前構造函數的層次,從而可以知道該調用更上層的構造函數。有沒有別的辦法來知道是那層構造函數調用了base()方法呢?殘酷的事實告訴我們,除了函數自身的caller屬性,沒有辦法知道是誰調用了自己。

    既然改變不了別人,那就改變自己!我們爲什麼就不能在運行中改變base()自身呢?

    事實上,第一層構造函數調用this.base()時,我們是可以過this.Type屬性知道地一層構造函數的,而this.Type.Base就是第二層構造函數。只是,第二層構造函數又會調用this.base(),其本來是想調用第三層的構造函數,但再次進入base()函數時,就無法知曉構造函數的層次了。

如果我們在第一層構造函數調用進入this.base()時,先改變this.base本身,讓其在下次被調用時能掉到第三層構造函數。完成這個變身動作之後再調第二層構造函數,而第二層構造函數再調用this.base()時就能調用到第三層構造函數了。這樣,只要我們在每次的base()調用中都完成一個自我的變身動作,就可以按正確的順序完成對構造函數的調用。這是多麼有趣的調用方式啊!

    於是,我們可以將原來的base()函數改寫成下面的形式:

    object.prototype.base = function()  //調用基類構造函數
    {
        
var Base = this.Type.Base;  //獲取當前對象的基類  
        if(!Base.Base)  //若基類已沒有基類
            Base.apply(this, arguments)     //則直接調用基類構造函數
        else    //若基類還有基類         
        {
            
this.base = MakeBase(Base);     //先覆寫this.base
            Base.apply(this, arguments);    //再調用基類構造函數
            delete this.base;               //刪除覆寫的base屬性
        }
;
        
function MakeBase(Type) //包裝基類構造函數
        {
            
var Base = Type.Base;
            
if(!Base.Base) return Base; //基類已無基類,就無需包裝
            return function()   //包裝爲引用臨時變量Base的閉包函數
            {
                
this.base = MakeBase(Base);     //先覆寫this.base
                Base.apply(this, arguments);    //再調用基類構造函數
            }
;
        }
;
    }
;


    原來的base()函數只有兩行代碼,而新的base()函數卻又十幾行代碼。看來爲了支持Opera瀏覽器確實也付出了代價,好在這些代碼只有十來行,代價並不是太大。當然,如果無須支持Opera瀏覽器,也就不必用這個base()函數了。

    在這個新的base()函數中,對this.base的覆寫實際上只是在this對象身上創建了一個臨時的base方法。這個臨時方法暫時遮住了object.prototype.base方法,而object.prototype.base方法卻一直存在。而每次對this.base的變身操作,都是針對這個臨時的方法的。當所有層次構造函數的調用都完成之後,即可刪除this對象的這個臨時base方法。

    其中的MakeBase()函數非常有意思,如果基類還有基類,它就返回一個閉包函數。下次this.base()被構造函數調用時,即調用的是這個閉包函數。但這個閉包函數又可能會調用MakeBase()形成另一個閉包函數,直到基類再無基類。

    如果說這是遞歸調用呢?卻並非那種函數自身對自身的直接或間接調用,而是調用一個函數卻返回另一個函數,再調用返回的函數又會在其中產生新的返回函數。如果說是函數式編程中的高階函數調用呢?這函數的嵌套階數卻是與類層次相關的不確定數,而每一個階梯都有新生成的函數。

這可真是閉包中嵌套着閉包,貌似遞歸卻又不是遞歸,是高階函數卻又高不可測。一旦整個對象創建完成,用過的內存狀態都釋放得乾乾淨淨,只得到一塵不染的新建對象。JavaScript玩到這樣的境界,方顯觀音大士的法力!

    下面就是是重寫後的完美甘露模型代碼:

http://www.leadzen.cn/Books/WuTouJavaScript/1/JS25.htm


 

<script type="text/javascript">
    
//定義類的語法甘露:Class()
    //最後一個參數是JSON表示的類定義
    //如果參數數量大於1個,則第一個參數是基類
    //第一個和最後一個之間參數,將來可表示類實現的接口
    //返回值是類,類是一個構造函數
    function Class()
    
{
        
var aDefine = arguments[arguments.length-1]; //最後一個參數是類定義
        if(!aDefine) return;
        
var aBase = arguments.length>1 ? arguments[0] : object; //解析基類
        
        
function prototype_(){}//構造prototype的臨時函數,用於掛接原型鏈
        prototype_.prototype = aBase.prototype;  //準備傳遞prototype
        var aPrototype = new prototype_();    //建立類要用的prototype
        
        
for(var member in aDefine)  //複製類定義到當前類的prototype
            if(member!="Create")    //構造函數不用複製
                aPrototype[member] = aDefine[member];
                
        
//根據是否繼承特殊屬性和性能情況,可分別註釋掉下列的語句
        if(aDefine.toString != Object.prototype.toString)
            aPrototype.toString 
= aDefine.toString;
        
if(aDefine.toLocaleString != Object.prototype.toLocaleString)
            aPrototype.toLocaleString 
= aDefine.toLocaleString;
        
if(aDefine.valueOf != Object.prototype.valueOf)
            aPrototype.valueOf 
= aDefine.valueOf;

        
if(aDefine.Create)  //若有構造函數
            var aType = aDefine.Create  //類型即爲該構造函數
        else    //否則爲默認構造函數
            aType = function()
            
{
                
this.base.apply(this, arguments);   //調用基類構造函數
            }
;

        aType.prototype 
= aPrototype;   //設置類(構造函數)的prototype
        aType.Base = aBase;             //設置類型關係,便於追溯繼承關係
        aType.prototype.Type = aType;   //爲本類對象擴展一個Type屬性
        return aType;   //返回構造函數作爲類
    }
;

    
//根類object定義:
    function object(){}    //定義小寫的object根類,用於實現最基礎的方法等
    object.prototype.isA = function(aType)   //判斷對象是否屬於某類型
    {
        
var self = this.Type;
        
while(self)
        
{
            
if(self == aType) return true;
            self 
= self.Base;
        }
;
        
return false;
    }
;
    
    object.prototype.base 
= function()  //調用基類構造函數
    {
        
var Base = this.Type.Base;  //獲取當前對象的基類  
        if(!Base.Base)  //若基類已沒有基類
            Base.apply(this, arguments)     //則直接調用基類構造函數
        else    //若基類還有基類         
        {
            
this.base = MakeBase(Base);     //先覆寫this.base
            Base.apply(this, arguments);    //再調用基類構造函數
            delete this.base;               //刪除覆寫的base屬性
        }
;
        
function MakeBase(Type) //包裝基類構造函數
        {
            
var Base = Type.Base;
            
if(!Base.Base) return Base; //基類已無基類,就無需包裝
            return function()   //包裝爲引用臨時變量Base的閉包函數
            {
                
this.base = MakeBase(Base);     //先覆寫this.base
                Base.apply(this, arguments);    //再調用基類構造函數
            }
;
        }
;
    }
;

    
//語法甘露的應用效果:    
    var Person = Class      //默認派生自object基本類
    ({
        Create: 
function(name, age)
        
{
            
this.base();    //調用上層構造函數
            this.name = name;
            
this.age = age;
        }
,
        SayHello: 
function()
        
{
            alert(
"Hello, I'm " + this.name + "" + this.age + " years old.");
        }
,
        toString: 
function()    //覆寫toString方法
        {
            
return this.name;
        }

    }
);
    
    
var Employee = Class(Person,    //派生自Person類
    {
        Create: 
function(name, age, salary)
        
{
            
this.base(name, age);  //調用基類的構造函數
            this.salary = salary;
        }
,
        ShowMeTheMoney: 
function()
        
{
            alert(
this + " $" + this.salary); //這裏直接引用this將隱式調用toString()
        }

    }
);

    
var BillGates = new Person("Bill Gates"53);
    
var SteveJobs = new Employee("Steve Jobs"531234);
    alert(BillGates);   
//這裏將隱式調用覆寫後的toString()方法
    BillGates.SayHello();
    SteveJobs.SayHello();
    SteveJobs.ShowMeTheMoney();
    
    
var LittleBill = new BillGates.Type("Little Bill"6); //用BillGate的類型建LittleBill
    LittleBill.SayHello();
    
    alert(BillGates.isA(Person));       
//true
    alert(BillGates.isA(Employee));     //false
    alert(SteveJobs.isA(Person));       //true
</script>


    當今的JavaScript世界裏,各式各樣的AJAX類庫不斷出現。同時,在開放Web API的大潮中,AJAX類庫作爲Web API最重要的形式,起着舉足輕重的作用。這些AJAX類庫是否方便引用,是否易於擴展,是否書寫優雅,都成了衡量Web API質量的重要指標。

    甘露模型基於JavaScript原型機制,用及其簡單的Class()函數,構造了一個非常優雅的面向對象的類機制。事實上,我們完全可以在這個甘露模型的基礎上打造相關的的AJAX類庫,爲開發人員提供簡潔而優雅的Web API接口。

想必微軟那些設計AJAX架構的工程師看到這個甘露模型時,肯定後悔沒有早點把AJAX部門從美國搬到咱中國的觀音廟來,錯過了觀音菩薩的點化。

    當然,我們也只能是在代碼的示例中,把Bill Gates當作對象玩玩,真要讓他放棄上帝轉而皈依我佛肯定是不容易的,機緣未到啊!如果哪天你在微軟新出的AJAX類庫中看到這種甘露模型,那纔是真正的緣分!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章