注意:如果您尚未閱讀過原來那篇老文章《悟透JavaScript》,請先行閱讀該文,以瞭解上下文關係。
在上面的示例中,我們定義了兩個語法甘露,一個是Class()函數,一個是New()函數。使用Class()甘露,我們已經可以用非常優雅的格式定義一個類。例如前例中的:
{
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
//定義類的語法甘露: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", 53, 1234);
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()函數改寫成下面的形式:
{
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
//定義類的語法甘露: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", 53, 1234);
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類庫中看到這種甘露模型,那纔是真正的緣分!