在這個js架構隨處亂跑的時代,你是否考慮過寫一個自己的架構?下面的內容也許會有點協助。
一個架構應該包含哪些內容?
1.語言擴充
大部分現有的架構都提供了這部分內容,語言擴充應當是以ECMAScript為基礎進行的,不應當依賴任何宿主環境,也就是說,作為一個架構的設計者,你應當保證你的語言擴充可以工作在任何宿主環境中,而不是僅僅適合瀏覽器環境。你必須保證把它放到WScript,SpiderMonkey Shell,Rhino Shell,Adobe ExtendScript Toolkit甚至Flash ActionScript等環境中都能正確的工作,舉個現實一點的例子setTimeout不可以出現在其中,你也不能用XMLHTTP載入指令碼運行,儘管它們看起來很貼近語言。保持這一部分的獨立性可以讓你方便的移植你的架構到其他宿主環境下。
2.資料結構和演算法
JS本身提供的內建對象非常有限,很多時候,架構應該提供一些資料結構和演算法來協助使用者更好的完成邏輯表達。但我認為隨便翻本資料結構或者演算法書用JS挑幾個實現了加到架構中是不負責任的,多數資料結構應當以庫的形式存在而非架構。架構中的資料結構應該足夠常用而且實現不是非常複雜的,可以考慮的如集合、雜湊表、鏈表、有序數組以及有序數組上的二分搜尋。對JS來說,對象是一個天然的字串雜湊表,而集合很容易在雜湊表上實現,因此只需要處理掉Object的內建方法,我們就可以實現一個高效的集合或雜湊表。
3.DOM擴充
JS主要應用於Web開發,目前所有的架構也都用於瀏覽器環境,那麼,瀏覽器端環境裡重點中的重點DOM當然也是架構的擴充目標了,如果一個架構不提供DOM的擴充,那麼其實基本沒什麼用處了。需要注意的是,DOM擴充也有w3c的標準可依,所以,不要嘗試為各種瀏覽器做一些奇怪的擴充,比如FF下面的element們的prototype,架構的編寫者應當無視它們。DOM擴充的主要任務之一是相容性,不同瀏覽器上的DOM實現相差很多,架構必須消除這些實現帶來的差異,提供統一的訪問方式。當然,做為架構,應當提供一些更為方便的介面,將宿主提供的DOM對象用js對象封裝是個不錯的想法,但是同時這也很可能會造成記憶體泄露,所以做這事之前,瞭解記憶體泄露是必要的。實際上,自己想象的擴充遠不如W3C的設計,比如如果你能更完整地實現XPath,你就能比JQuery做的更好。
4.AJAX擴充
大部分現有架構出現的原因都是因為AJAX,所以如果你想設計一個受歡迎的架構,AJAX是必須要做的。跟DOM擴充很相似,AJAX擴充的主要任務是相容和記憶體泄露,對AJAX的核心組件XMLHttpRequest對象,必須在IE6中使用ActiveX建立,而ActiveX又有各種版本,而隨之而來的記憶體泄露和相容性變得非常麻煩,比如:事件函數名大小寫、this指向、事件函數的null賦值。處理好這些相容性的基礎上,可以做進一步的工作,提供一些常用的實現。應該指出的是,除非你確定你提供的介面比原來的更好,否則不要改變原來的XMLHttpRequest對象的介面,比如寫一個Request函數來代替open和send,如果你不清楚W3C的專家們為什麼這麼設計,請不要把他們想象成傻瓜。我想自己另外寫一個相容且記憶體安全的XMLHttpRequest加入到自己架構的命名空間裡,使它從外部看上去跟W3C描述的XMLHttpRequest一模一樣是不錯的辦法,對XMLHttpRequest我認為唯一可以考慮的修改是提供onsuccess事件。當然針對一些複雜功能的AJAX擴充也是可行的,比如HTMLHttpRequest類似的擴充可以讓AJAX初學者喜歡你的架構。
5.效果
時間效果是最能刺激使用者神經的,漸隱、緩動、滑動、色彩坡形這些都很不錯,其實技術難度也不是很高。拖動效果則是瀏覽器中一個很重要的效果,用滑鼠事件來類比本來很容易,不過相容和setCapture也是很麻煩的事情。這一部分內容量力而為就可以了。
7.指令碼管理
因為大家非常喜歡C++風格的include或者JAVA風格的import,很多架構提供了基於AJAX的指令碼管理,不過同步載入的效率問題是巨大的。之前我們曾經作過各種嘗試,希望找到一個瀏覽器中不用XMLHTTP載入外部js的方法,但是最後得出的結論是:不可能。
關於這個,略微思考就可以知道,Java C++ C#都是編譯型語言,include 和import都是編譯期處理,在js中做到類似的事情是不太可能的。為了實作類別似的功能,我想我們可以利用服務端程式或者編寫一個文本工具來實現。
YUI將所有的js檔案依賴關係提取出來的做法是可行的,不過這不能算是include的實現方式了,維護依賴關係不是一件很簡單的事情。
8.控制項
EXT的成功告訴我們:提供優質的控制項才是架構的王道。你不能指望優質的擴充會吸引更多使用者。多數人只關心如何快速完成手邊的工作。當然不是所有架構都要提供這部分內容。控制項好壞取決於能力和美工,不過至少要保證架構裡的控制項不會記憶體泄露。
架構設計的若干原則:
1.不要做多餘的事
對這架構設計來說,我認為一個非常必要的原則就是不要做多餘的事情,舉個極端的的例子:
function add(a,b)
{
return a+b;
}
這樣的代碼如果出現在架構中,就是個十足的笑話。不過大多數時候,事情不是那麼明顯,很多架構試圖用某種形式在JS中"實現"OOP,但是實際上,JS本身是OO的(ECMA262中明確指出來的,不像某些人所說是基於對象云云)只是有一些文法跟Java等語言不同。那麼顯然這些OOP的"實現"其實是跟上面的例子一樣的道理。另一個例子是Array.prototype.clone
Array.prototype.clone=function(){
return this.slice();
}
2.慎用prototype擴充
很多架構利用修改原生對象的prototype來做語言擴充,但我認為應當小心地看待這件事,毫無疑問這將造成一定的命名汙染,你無法保證架構的使用者或者與你的架構共存的其他架構不會使用同樣的名字來完成其他的事情。特別需要注意的是,Object和Array這兩個對象的prototype擴充格外的危險,對Object來說,如果Object被修改,那麼架構的使用者將無法建立一個未被修改的乾淨的對象,這是一個致命的問題,尤其如果你的使用者喜歡用for in來反射一個對象的屬性。Array.prototype修改的危險來自js一個不知有意還是無意的小小設計,對原生的Array來說,任何情況下for和for in的遍曆結果是相同的,而因為無法控制自訂的屬性是不可枚舉的,任何Array.prototype的修改都會破壞這種特性。一方面,我認為不應當推薦用for in遍曆數組,這其中包含著錯誤的語義。另一方面,架構的設計者必須尊重這些使用者,因為對於ECMA所定義的文法而言,它是正確的做法。其中包含著這樣一個簡單的事實:假如你的架構中修改了Array.prototype,那麼一些之前能正確工作的代碼變得不可正確工作。
直接修改prototype看上去非常誘人,但是在考慮它之前應當先考慮兩種可能的方案:
(1)函數
提供一個以對象為第一個參數的函數比如 Array.prototype.each =>
function ForEach(arr,f)
{
if(arr instanceof Array)/*...*/;
}
(2)繼承
以return的形式繼承一個內建對象 比如考慮Array.prototype.each=>
function ArrayCollection()
{
var r=Array.apply(this,arguments);
r.each=function(){/*......*/};
return r;
}
套用一句名言,不要修改原生對象的prototype,除非你認為必要。不過修改原生對象的prototype確實有一些特殊的用途(就是"必要的情況"),主要體現在2點:文字量支援和鏈式表達。舉一個例子可以體現這兩點:
var cf=function f(a,b,c,d)
{
/*........*/
}.$curry(3,4).$curry(5).$run();
如果希望實作類別似上面的表達方式,可能就需要擴充Function.prototype,權衡一下的話,如果你認為命名汙染的代價是值得的,那麼也是可以提供給使用者的。
一個比較討巧的辦法是把選擇權利交給使用者,提供一個擴充器:
function FunctionExtend()
{
this.$curry=function(){/*......*/};
this.$run=function(){/*......*/};
}
如果使用者喜歡可以FunctionExtend.apply(Function.prototype); 如果不喜歡擴充 則可以
var r=function(){/*......*/};
FunctionExtend.apply(r);
3.保持和原生對象的一致
不知你有沒有注意到,內建對象Function Array等都有這樣的性質:
new Function()跟Function的結果完全一致(String Number Boolean這種封裝型對象沒有這樣的性質)
如果架構中提供的類也具有這種性質,會是不錯的選擇。這僅僅是一個例子,如果你注意到了其他細節,並且讓架構中的類和原生對象保持一致,對使用者來說是非常有益的。
4.尊重語言 尊重使用者
編寫架構應該尊重依賴的語言環境,在對原有的元素修改之前,首先應該考慮到原來的合理性,任何語言的原生環境提供的都是經過了精心設計的,在任何修改之前,至少應該考慮這幾點:效率、命名規範、必要性、與其他功能是否重複。如果你沒有付出至少跟語言的設計者相當的工作量,你的做法就是欠考慮的。
編寫架構也應該尊重使用者的所有習慣,將編寫者的喜好強加給使用者並不是架構應該做的事情。架構應該保證大部分在沒有架構環境下能啟動並執行代碼都能在架構下正常工作,這樣使用者不必為了使用你的架構而修改原有的代碼。
5.規範命名和使用命名空間
減少命名汙染可以讓你的架構跟其他架構更好地共存。很多架構使用了命名空間來管理,這是良好的設計。命名應該是清晰且有實際意義的英文單詞,如前面3所述,為了保持和原生對象的一致,命名規則最好貼近原生對象,比如類名第一字母大寫,方法名用駝峰命名。捎帶一提prototype中的$實在是非常糟糕的設計,無法想象$出現的目的僅僅是為了讓使用者少寫幾個字母。這種事情應該交給你的使用者在局部代碼中使用。