原文地址:http://jqueryvsmootools.com/index_cn.html
大部分最近才剛接觸JavaScript的人會面臨到的困難是該選擇哪個套件(library)或是該先學哪個套件。如果你在一間公司裡上班,那麼可能公司已經有一套固定使用的套件,若是在這種情況下,問題就沒那麼重要。如果你的公司選擇使用MooTools而你自己已經習慣使用jQuery,那麼這篇文章也許對你還是有些幫助的。
每天在 Twitter上 我看到 一堆人討論著MooTools好還是jQuery的話題。這篇文章希望能幫助你做出這決定。
我是個MooTools的開發人員。我也專注於MooTools這個framework。我的部落格是有關MooTools。我寫過MooTools的線上文件以及一本有關MooTools的書。很明顯的我的立場是有一點偏頗的。我也必須說明我並不常用jQuery。如果你是個jQuery的開發員,同時看到我文章裡任何謬誤之處,請聯絡我並不吝給予指教錯誤之處。我的目的是讓文章正確且有所助益而非去推銷特定的framework。
透過我的說明,讓大家明白這兩個frameworks之間的差異進而能幫助大家做出一個選擇。我首先要說的是,兩個frameworks都是很好的選擇,你並不會選到一個糟糕的framework。兩者各有其強項與弱項,但是都是很棒的。除此之外還有很多也很優秀的frameworks值得考慮,如Dojo,Prototype, YUI,Ext 等等的。這些選擇基本上也取決於自己的習慣跟需要完成甚麼樣的東西而定。這篇文章的重點在於MooTools跟jQuery之間的比較,因為我看到越來越多的人在這兩者之間猶豫不決著。最後我要聲明的是,我並不是想要說服誰誰誰從某個framework轉換到另一個,這兩個frameworks其實有很多有趣的地方值得我們學習。你可以從這個公告看到我寫這篇文章的由來。
jQuery 核心 | MooTools 核心 | |
---|---|---|
檔案大小 | 55.9K | 64.3K |
功能 | ||
授權 | MIT & GPL | MIT |
DOM 工具 | yes | yes |
動畫效果 | yes | yes |
事件處理 | yes | yes |
CSS3 選擇器 | yes (Sizzle引擎) | yes (Sly引擎) |
Ajax | yes | yes |
原生物件延伸 Native Extensions (不含Element) | 10幾個關於 Array, Object, 以及 String的延伸 | 約70個 Array, Object, String, Function, 以及 Number 的延伸 |
繼承 | 沒有直接支援繼承 | 提供 Class 構件式 |
其他考量項目 | ||
plug-ins | 上百個非官方 plug-ins:plugins.jquery.com | 目前約四五十個官方 plug-ins: mootools.net/more, &mootools.net/plugins |
官方UI套件 | yes | no |
資料來源: jquery.com, mootools.net, 與 wikipedia.com.
如果你去jQuery的網站看,它最上面的標語就寫著:
jQuery是個簡潔快速的JavaScript函式庫,能透過簡化HTML的操作、事件的處理、動畫效果以及Ajax互動來加速網頁開發。jQuery是一個可以改變你寫JavaScript方式的函式庫。
...然後如果你到了MooTools的網站,你也可以找到這段話:
MooTools是個精簡、模組化同時也物件導向的JavaScript框架,設計給中等與進階的JavaScript開發員使用。透過其優美、詳細而條理分明的API,可以讓您寫出強大、富有彈性且跨瀏覽器的程式。
我想這就說明了全部了。如果你問我(因為你正在讀這篇文章,所以我假設你會問囉):重點不在於哪個比較好,哪個比較差,而是在於這兩者的特性之間,該如何取捨衡量?這兩個框架(frameworks)試圖要做的事情並不相同,在功能上它們或有重疊但目標並非一致。
jQuery的那段敘述講的是關於HTML、事件、動畫、Ajax與網頁開發,MooTools那段說的是有關物件導向、強大且彈性的程式架構。jQuery要改變你寫JavaScript的方式,而MooTools則是為了中高階JavaScript開發員而設計。
這其中值得思考的是關於框架與工具包(framework vs. toolkit)的概念差異。MooTools是一個用JavaScript原本應有的方式來寫JavaScript的框架(framework)。其立意是實作一個跟JavaScript非常相似的API並且針對各個部分加以強化,而不只是針對DOM而已。jQuery則比較像是包好了很多很方便的函式在一個工具包裡(toolkit),這個自成一個體系的工具包讓DOM的操作變得更簡單易用。而DOM剛好就是大多數的人寫JavaScript時花最多時間的地方,所以在大多數的情況下,jQuery就是你最好的選擇。
大部分你用MooTools所寫的程式還是跟JavaScript很像,如果你對JavaScript這個程式並不是很感興趣,那麼學MooTools對你來說就會是很討厭的事情。如果你對JavaScript有興趣,也想知道它哪裡有趣、強大而多樣,那麼我個人認為MooTools是個較好的選擇。
首先,jQuery是非常非常容易學會的。它的撰寫風格如同口語般幾乎不像個程式語言。如果你要的就是快速搞定又不用刻意學習JavaScript的話,jQuery可能對你而言是較好的選擇,MooTools並不具備這些好處。我必須承認如果你是個JavaScript新手,MooTools會有點難上手。加上有很多現成的資源或教學可以幫助你學會jQuery,至少比MooTools多。
如果你比較看看jQUery(jQuery的討論版)跟MooTools的社群(irc, mailing list),你馬上就會發現:一) jQuery的社群資源與數量遠大於MooTools(我認為是因為之前說的那個原因易學以及...)。二) 她們比較積極推廣。如果你用使用的人數、Google搜尋的次數以及賣出的書量來衡量jQuery與MooTools,你會看到jQuery是遙遙領先的。
為了要完整說明為何你可以考慮用MooTools的原因,我必須先說一下這兩個frameworks在做什麼事情。最終你會選擇哪個framework還是取決於你想要達成什麼目的以及你想用什麼方式寫程式(甚至關乎你是否喜歡程式,或至少喜歡JavaScript)。
抉擇的過程裡,你得先問自己想要用JavaScript來做什麼。我們可以先想想純JavaScript的情況,不使用任何的framework;就是傳統的JavaScript程式。JavaScript提供一些原生的物件像是 Strings,Numbers, Functions, Arrays, Dates, Regular Expressions, 等等,同時也提供了一個繼承的模型 - 一個有點難懂的模型叫做原型繼承(prototypal inheritance,我稍後會作說明)。這些基本架構跟繼承概念就是足以支撐著各種程式語言的基本要素,而且這些要素跟瀏覽器、網頁、CSS或HTML都沒有任何關聯。所以其實JavaScript是可以用來寫出各種程式的,不管是井字遊戲、西洋棋、相片編輯程式或是個網站伺服器。只是剛好百分之九十九的JavaScript都在瀏覽器上執行,所以我們通常就把它當作是瀏覽器專用的程式了。
絕大部分的JS都應用在瀏覽器跟DOM,但其實JavaScript是個非常健全且多樣的程式語言,如果能夠理解這點的話,會有助於你了解MooTools跟jQuery之間的差異。
如果你認為JavaScript做的事情就是"指定頁面中的某個東西,然後對它做一些動作",那jQuery可能就是最好的選擇了。jQuery提供了一個非常好的架構讓你用幾乎不像是程式語言的方式去描述頁面中的行為。你還是可以用JavaScript的其他功能來寫出你想要的程式,但是如果你的重點在DOM - 動態改變CSS、動畫效果、透過AJAX來取得內容等等,你需要寫的大部分程式都可以用Jquery完成,其餘未包括的部份就會是原本JavaScript的寫作方式。此外jQuery也提供一些非DOM相關的程式,例如繞陣列迴圈(iterating)的方法 - $.each(array, fn) - 或是用$.trim(str) 除掉字串前後空白的函式。這些公用程式並沒有很多,不過那也沒差,因為大部分情況下,如果你只是從DOM裡面指定了幾樣東西,繞了迴圈,變更了某些細節(插入HTML、改變樣式或是定義一個滑鼠點擊的事件等等),其實並不需要那麼多公用程式。
不過如果你用比較廣泛的角度來看的話,jQuery並沒有把心思放在DOM以外的事物上。這也是它如此易學的原因之一,但同時也限制了從其他方面來幫助你撰寫JavaScript。它除了扮演著一個完整的DOM程式語言之外,便再無二心,沒有在繼承物件概念或是提供基本型別的公用程式上多下功夫,因為它本來就沒這個必要。如果你想在字串、時間、正規表示式、陣列或函式之間周旋的話,可以。只不過那並非jQuery的使命,JavaScriprt一直都在那,隨時要用都可以的。jQuery讓你能夠輕鬆駕馭DOM,但其他的東西就不是它的範疇了。
這點跟MooTools有很大的不同。MooTools把範疇放大到整個JavaScript語言,而不單單只是針對DOM而已(據我所知,MooTools也提供跟jQuery相同的功能,但是用很不一樣的方法寫的)。如果jQuery讓你能對DOM為所欲為,MooTools則是讓你能夠對整個JavaScript為所欲為,這也是它學習難度較高的原因之一。
JavaScript裡面有些很棒的東西。首先,它是個函數程式語言。這代表著function被視為高階物件,像其他物件 - 如字串或數字般可以被互相傳遞。當你用這種方式來寫程式,很多模式跟方法可以藉由這個設計理念獲得最好的結果。兩者之間的差異:
for (var i = 0; i < myArray.length; i++) { /* do stuff */ }
以及
myArray.forEach(function(item, index) { /* do stuff */ });
JavaScript有一種 繼承模型,在程式語言的世界裡,不能說獨特但也算是相當罕見。JavaScript是用 原型繼承的方式來做,而非類別(class)在延伸次類別的繼承方式,這表示物件是直接繼承另一個物件的原型。當你參照一個物件的property,程式會先從物件本身尋找是否有此property,如果沒有的話,就去找該物件的父親。這就是一個陣列可以有method的原理。當你寫了:
[1,2,3].forEach(function(item) { alert(item) }); //this alerts 1 then 2 then 3
"forEach"這個方法並不存在於你所定義的陣列([1,2,3])裡面,它存在於所有陣列的原型中。當你參照forEach這個方法時,JavaScript會在你定義的陣列中去找,如果沒有的找到的話就去陣列的原型去找。這表示說,並非所有陣列都在記憶體中有forEach這個方法,它只要在陣列的原型裡放一次就可以了。這種方式非常有效率也非常非常強大。(補充:MooTools裡面forEach的對應method是each)
JavaScript有一個特別的字眼:"this"。我很難在這邊簡單的定義甚麼叫this,但是一般來說this就是指目前method的所有者。this允許物件可以在其所屬方法運行時參照到物件自己本身,也正是this存在的目的。當你產生很多子物件然後又有很多父物件的實體(instances)時,這就變得很重要了,否則還能甚麼其他方式來參照自己?當一個方法(method)原本屬於父物件而非子物件,this這個關鍵字就能讓實體們都能參照到它們自身。(這裡有this更完整的解釋,還有Mozilla的)
this可以讓那些繼承而來的子物件參照到它們自身,但是有時候也可能需要用this參照到其它的東西,你就要指定一個不同的this到方法(method)裡,這就叫做榜定(binding)。陣列的each方法的第二個參數可以讓你指定一個物件做榜定(binding)。可以參考一下這個範例傳了一個不一樣的 "this":
var ninja = { weapons: ['katana', 'throwing stars', 'exploding palm technique'], log: function(message) { console.log(message); }, logInventory: function() { this.weapons.each(function(weapon) { //we want "this" to point to ninja... this.log('this ninja can kill with its ' + weapon); }, this); //so we pass "this" (which is ninja) to Array.each } }; ninja.logInventory(); //this ninja can kill with its katana //this ninja can kill with its throwing stars //this ninja can kill with its exploding palm technique
上面的範例裡面,我們把ninja榜定(就是在logInventory裡面的this)在傳給陣列的那個方法(method),所以在each裡面我們可以參照到ninja的log的屬性。如果我們沒這麼做的話,this得到的參照就會是window了。
這些只是幾個範例,用來說明JavaScript提供了強大多樣的繼承、自我參照、榜定跟原型特性。不幸的是,原本的JavaScript並沒有把這些強大的特點凸顯出來,這也是MooTools之所以發展的起頭。MooTools把這幾種類型的模式變得簡單也更和善。最終你會寫出更為抽象的程式,長遠來看這會是好事情 - 也會是很棒的程式。去理解這些模式的價值以及如何正確地使用它們是很需要花費心力的,但是從好的方面來看,你所寫的程式會具備高度的重複使用性也會非常容易維護。這些好處我等等會稍做說明。
因為MooTools專注在增加JavaScriptAPI的穩定性與連貫性,所以在"改變你寫JavaScript的方式"這件事情上就沒有太多著墨,整體來看它卻是降低了JavaScript使用上的不便。MooTools是JavaScript語言的擴充,也試著把JavaScript導向原本應走的路上。整個架構的核心有重要的一部份是在加強Function、Stirng、Array、Number、Element跟其他的原型物件。另一個重點是它提供了一個函數叫類別(Class)。
現在,對人們來說,Class看起來像是要重建一個像Java或是C++那種正統的繼承式模型,其實並不是那麼一回事。這個Classn所做的是幫我們更簡單地存取JavaScript的原型繼承模型並且更加凸顯它的優勢。我要說明的是這概念並不是MooTools才有的(其他的一些frameworks也有提供類似的功能)。但是jQueryy並沒有這些的設計概念,jQuery沒有所謂繼承的系統也沒有任何加強原生物件(Function、String等)的架構。這並非jQuery的缺陷,因為作者其實可以輕而易舉的增加這些東西進來;而是他們是以不同的目標來設計這個工具包(toolkit)套件。MooTools的目的是讓JavaScript變的有趣,而jQuery是要把DOM變得有趣,所以它的設計者設定了這樣的範疇來達到目的。
這也是為什麼jQuery比較有親和力,它並沒有要求你把JavaScript從裡到外都搞懂,它不會把你丟到底層去跟原型繼承架構、榜定、"this"和原生原型(native prototypes)打交道。當你開始從官網接觸jQuery,你可以看到它的第一個範例:
window.onload = function() { alert("welcome"); }
and here's the third:
$(document).ready(function() { $("a").click(function(event) { alert("Thanks for visiting!"); }); });
如果你讀過MooTools的書或是教學(我寫的那兩個),裡面開頭的章節就很不同。如果要學習怎麼使用MooTools的話,你必須從一些基本的概念開始看起,例如Class,雖然你也可以略過直接開始學有關特效或是DOM的部分。而且我也必須承認:如果你是寫程式的新手或你想要快速架站,而非得要先了解JavaScript的一些細節,可能對你來說jQuery會比較友善的多。
反之,如果你想要好好學習JavaScript這個語言,MooTools是一個很好的管道。MooTools實做了很多JavaScript以後會有的功能(很多在Native的方法正是JavaScript 1.8或更新版本會有的)。如果你是老手了,特別是熟悉物件導向以及函數式程式語言(functional programming)的人,MooTools有很多令人振奮且驚艷的設計模式(design patterns)。
如果你看一下jQuery所提供的功能,你會發現通常MooTools也有提供相同的功能。或如果你看一下MooTools的功能,你會發現jQuery通常沒辦法達到相同的目的,因為jQuery專注的是DOM相關功能,MooTools提供的功能則涵蓋的層面比jQuery還廣些。不過jQuery也不會阻礙你去實做那些MooTools才能達到的功能。舉例來說,jQuery並沒有提供繼承的架構,但是沒關係,如果你想要的話還是可以結合MooTools 的Class來達到(或自己寫一個)。甚至也有人寫了一個繼承的jQuery外掛(我沒有用過不過我想它應該也能提供相同的功能)。
如果我們回頭看一下上面jQuery的那個範例:
$(document).ready(function() { $("a").click(function(event) { alert("Thanks for visiting!"); }); });
如果我們要轉成MooTools的語法,就會變成:
window.addEvent('domready', function() { $$('a').addEvent('click', function(event) { alert('Thanks for visiting!'); }); });
非常相似吧?
或是這個比較複雜的jQuery範例:
$(document).ready(function() { $("#orderedlist li:last").hover(function() { $(this).addClass("green"); }, function() { $(this).removeClass("green"); }); });
and in MooTools:
window.addEvent('domready',function() { $$('#orderedlist li:last-child').addEvents({ mouseenter: function() { this.addClass('green'); }, mouseleave: function() { this.removeClass('green'); } }); });
也是非常類似。我想說明的是,MooTools的版本語法更為清楚,不過也因此較為累贅。MooTools的程式閱讀很清楚可以知道我們增加了兩個events,一個是滑鼠移入物件時的,一個是滑鼠離開物件的。而jQuery則較為簡潔,它的hover方法一次接收兩個functions,第一個是滑鼠移入物件時用,第二個是滑鼠離開物件時用。我個人比較偏好MooTools程式的可閱讀性,不過那是主觀的看法。
我有時候會覺得jQuery的語法過於艱澀隱晦,一個方法(method)裡可接收兩個function這回事我光是用看的的話,並非那麼容易理解。由於我對MooTools很熟悉,這說法多少有點不公平,因為讀MooTools的程式對我來說很簡單。不過MooTools方法與類別的命名方式很有名符其實的味道,這點我很欣賞。MooTools幾乎都是用動詞來命名,留下一絲絲疑惑是對"甚麼"做動作。當然在撰寫每一種程式語言時,你都需要去看文件來查語法,我意思不是說MooTools就不用,我只是覺得MooTools的API的命名邏輯比較連貫一致。
可是如果你比較喜歡jQuery式的語法怎麼辦?MooTools也是有這種能力把程式變得更合你胃口,如果我們想在MooTools裡面實作jQuery的hover,我們只要加上這段:
Element.implement({ hover : function(enter,leave){ return this.addEvents({ mouseenter : enter, mouseleave : leave }); } }); //然後你就可以用跟jQuery相同的語法: $$('#orderlist li:last').hover(function(){ this.addClass('green'); }, function(){ this.removeClass('green'); });
實際上確實有MooTools的plug-ins在幫你完成這些動作:jQuery syntax for MooTools。MooTools著重在延伸能力上,這意味著你可以做任何你想做的事情,這是jQuery沒辦法辦到的。MooTools可以模仿jQuery,但是jQuery沒辦法模仿MooTools,如果你想要用類別或是延伸的原生原型物件等等MooTools的功能,你就得自己手動去寫額外的程式。
我們來試試別的東西看看,這邊有一些jQuery的範例(從jQuery教學裡面擷取的):
$(document).ready(function() { $('#faq').find('dd').hide().end().find('dt').click(function() { $(this).next().slideToggle(); }); });
我個人並不是很推崇這個範例的語法,光是看上面那段程式碼我很難立刻了解在幹嘛。最明顯的部分是那個.end在做甚麼以及後面連接的.find是啥,跟.end又有甚麼關聯。jQuery文件可以找到關於.end很清楚的解說(重設目前選擇的物件(selector),也就是#faq),不過我是覺得滿怪的。當我在用jQuery的時候,我自己常常不太能確定一個方法(method)會回傳甚麼東西給我。不過很明顯的,這並沒有困擾到大家,因為jQuery有著一大票的愛用者,所以我也把這個歸類為個人偏好之一。
我們來看看上面那段程式碼如果在MooTools裡面會怎麼寫:
window.addEvent('domready', function() { var faq = $('faq'); faq.getElements('dd').hide(); faq.getElements('dt').addEvent('click', function() { this.getNext().slide('toggle'); }); });
當然,MooTools還是比較累贅些,但是意思就更明確。同時也可以看到,jQuery用.end來回傳#faq這個物件,而MooTools的設計模式是把#faq放到一個變數裡。當然MooTools也可以寫出jQuery那樣的連續技(Chaining):
item.getElements('input[type=checkbox]') .filter(function(box) { return box.checked != checked; }) .set('checked', checked) .getParent()[(checked) ? 'addClass' : 'removeClass']('checked') .fireEvent((checked) ? 'check' : 'uncheck');
不過說真的,像這樣把一堆程式碼跟判斷塞到一個domready宣告裡,不管是用哪一種framework都不是個很好的做法。如果把這些邏輯都封裝成可以重複利用的片段會好很多。
當你在執行一個網站專案時,可重複利用的程式碼是很誘人的。只需要寫一點點程式就可以選取所需的DOM,然後隱藏掉某些、變更幾個屬性以及增加幾個滑鼠的event就搞定一切。這種開發方式會非常的有效率、非常的快。不過把你的程式碼都寫在domready裡面的問題在於:最後你會在不同地方寫了一堆在做同樣事情的程式碼。如果我們用上面那個FAQ的範例來看,我們其實可以輕易地套用相同的程式碼套在不同的頁面中類似的結構。難道我們每次遇到這種結構時都要重複做一次相同的事情嗎?
有個很簡單的方法就可以把它變成可重複利用,我們只要把這段程式碼包裝起來然後給個參數讓它接收。在jQuery裡面可能會長這樣:
function faq(container, terms, definitions) { $(container).find(terms).hide().end().find(definitions).click(function() { $(this).next().slideToggle(); }); }; $(document).ready(function() { faq('#faq', 'dd', 'dt'); });
這樣一來程式會好很多,有兩個很重要的原因:
jQuery其實有在針對可重複使用的"wigdets"上做了一些調整。他們鼓勵人們使用jQuery plug-ins,而不是用上面那個範例(其實滿粗糙的)的方式。然後會長成像這個樣子:
jQuery.fn.faq = function(options) { var settings = jQuery.extend({ terms: 'dt', definitions: 'dd' }, options); //"this" is the current context; in this case, the elements we want to turn into faq layouts $(this).find(settings.terms).hide().end().find(settings.definitions).click(function() { $(this).next().slideToggle(); }); return this; };
接下來你就可以這樣用:
$('#faq').faq();
從上面那個範例來看,寫一個faq 的plug-in跟寫成一個普通的function沒有很大的差別。是的,普通的function沒有放在一個全域的命名空間(global namespace),不過我們也可以創一個nampespace,然後把function放進去。像plug-in那樣依附在jQuery中,我們就可以把其他的jQuery方法用連鎖動作串再一起。另外一個好處是,function裡面呼叫this就會得到目前的元素(current context)。用plug-in的作法,就會讓function看起來像是jQuery的一部份,但是除此之外plug-in基本上就是一個單一的function加上能存取到目前的jQuery元素(context)、操作那些元素然後再把那些元素傳給連鎖動作中的下一個method。它並不複雜,所以每個人都能輕鬆的寫出jQuery plug-ins,它們就是functions而已。
另外注意的是,jQuery也可以寫一些較為複雜,包含methods跟狀態的plug-ins。jQuery UI有支援這種模式,不過寫法跟基本plug-in(像faq那個範例)不同,取而代之的是你會在jQuery上附加一個具有methods跟properties的物件(如$.ui.tabs)。它有個快捷方式讓你直接使用(如$(selector).tabs()),所以你還是可以使用連鎖動作,就像faq那個plug-in一樣。只不過它並不是回傳該物件本身($.ui.tabs),而是回傳jQuery 目前的元素(context),所以當你要存取tab的methods時,你必須再次呼叫一次selector。你必須再執行一次selector然後再呼叫function的名稱:$(selector).tabs('add', url, label, index); ,而不是直接呼叫類似像myTabInstance.add(url, label, index) 的東西。這意味著你執行了兩次選擇器(除非你用個變數來參照),你也沒有可以指向add這個method的參照讓你做像是綁定(bind)或是延遲(delay)的動作。這篇文章焦點在於MooTools跟jQuery的核心程式,而雖然jQuery UI有提供這些功能,但是jQuery本身並沒有。
在MooTools裡面當你要定義一個模式(pattern)時,你會用Class或是實作一個方法到原生的物件上(如String)。
MooTools介於一個獨有語法與擴充JavaScript原本的設計模式之間,它走的是一個較為中庸的道路,而非給你一個跟JavaScript完全迥異的程式語言。它的其中一個方式是去擴充程式本身跟DOM原生物件的prototype,如果你想要有個去掉字串空白的功能,MooTools鼓勵你在String物件上增加一個method(MooTools已經有提供String.trim,你不需要自己再寫一個)。
String.implement({ trim: function() { return this.replace(/^\s+|\s+$/g, ''); } });
這意味著你可以直接在字串上使用: " no more spaces on the end! ".trim(),然後你就會得到 "no more spaces on the end!" 的結果。有些人可能會說,去實作原型(native prototype)並不妥當,這也是為什麼MooTools跟Prototype.js沒辦法同時並存,任何兩個會動到原生原型的frameworks都是沒辦法並存的。如果我定義了,而在另一個函式庫裡面也有相同的東西,那最後定義的會覆蓋原先的。某種程度來說,這也跟window的全域命名空間的問題類似,這就是JavaScript的特性。這也是JavaScript 1.8增加新功能的方式,它們都是透過prototypes。
MooTools的開發者包好了一個完整的framework讓你可以很輕易的擴充你想要的功能,其出發點是讓你把這個framework放到網頁裡面去使用,而不是讓你放其他的framework,要求訪客在一個頁面下載兩個不同的framework其實是有點不禮貌的。一次套用兩個framework唯一可能的理由是,兩個frameworks的plug-ins你都想要用。MooTools作者(包含我自己)的想法是,如果你想要的某個plug-in是你所選的那個framework裡面沒有的,那可能比較合理的作法是花時間去做出來,而非要求使用者去多下載一個framework的檔案。
一旦你學會了JavaScript的運作原理跟擴充原生物件的強大功能,一個全新層級的程式寫作方式就此開展。你可以寫plug-ins來改變Elements、Dates或是Functions。某些人可能會說,這種添加方式是一種對原生物件的汙染,我必須主張這種方式就是JavaScript原本的意圖。它是程式語言設計的特色,透過添加methods到原生物件上,你的程式碼會變得更簡潔,更有區隔性。jQuery也有這麼做,不過只限於強化jQuery自己本身的物件。
雖然你可以輕鬆地在jQuery物件上連續呼叫methods,但是在其它一般物件上,你就不能用同樣的方式。舉例來說,如果你要一行一行地重複trim掉字串,在jQuery你可能得這麼做:
$.each( $.trim( $('span.something').html() ).split("\n"), function(i, line){alert(line);});
但是由於MooTools修改過prototypes,所以你可以:
$('span.something').get('html').trim().split("\n").each(function(line){alert(line);});
這個範例很清楚地示範了修改prototypes有多麼強大。連鎖動作並不是只能在DOM,連鎖可是個很有用的東西,MooTools讓你可以在任何物件上做連鎖動作,包括一次對多個元素(elements)執行一個method。
這裡的重點是MooTools的精隨在於它主張你可以寫出任何你想要的東西。如果有個功能是裡面沒有的,你可以擴充它並增加你自己要的。它的工作並不提供大家任何可能該有的功能,它是提供一個工具讓你可以為所欲為。它有很大一部分在做的事情是把擴充原生prototypes這件事情變得更簡單,善用原型式繼承的優點。你也可以用傳統的JavaScript來寫出這些東西,但是MooTools把它變得更簡易和善。
儘管它名字叫MooTools,它的Class功能並不是真的類別也不是創造一個類別。它的設計模式也許會讓你想起傳統的程式語言,但Class的確是都是跟物件與原型繼承相關的。(很不幸地,用"class"這個字眼是用來描述這些東西最方便的方式,所以在這篇文章中,當我提到"class"的時候,就是指那些會回傳物件的函數,而這些物件就是我要用來"實體化"且繼承自某個原型。)
要建立一個class,你可以把一個物件傳進這個類別構建式,像這樣:
var Human = new Class({ initialize: function(name, age) { this.name = name; this.age = age; }, isAlive: true, energy: 1, eat: function() { this.energy = this.energy + 1; //same as this.energy++ } });
你把一個物件丟給Class(在上面,我們把一個有"isAlive"跟"eat"成員的物件傳進去),這個物件就會變成任何類別實體(instance of class)的原型。要建立一個實體,你只要:
var bob = new Human("bob", 20); //bob's name is "bob" and he's 20 years old.
現在我們得到一個Human. bob實體,裡面有我們在建立我們自己的Human類別時所定義的屬性。但是重點是bob的那些屬性是透過繼承而來的,當我們參照到bob.eat時,bob並非真的具有這個屬性。JavaScript先看bob,發現他沒有eat這個方法,所以就從繼承鍊找到它,也就是從我們之前建立的Human類別裡面,energy這屬性也是同樣的道理。一開始這看起來有些潛在的壞處;我們並不希望每次bob吃(eat)的時候,每一個被建立的human都會得到一次energy。有一點需要知道的是,當一開始給enegey值的時候,bob會擁有自己的值,我們也不再從原型(prototype)中去尋找值了。所以第一次bob吃(eat)的時候,他會得到自己的energy值(enery=2)。
bob.eat(); //bob.energy == 2
bob的name跟age都是獨有的;在initialize初始化的時候就分配給bob了。
這整個模式看起來可能對你來說有點怪,但是其價值在於我們可以為某種特定模式(pattern)定義功能,然後每次需要使用時便初始化這個東西。每個實體(instance)都有自己的狀態。所以如果我們建立另一個實體,那麼兩個實體個字都會是獨立的,且繼承自同一個基本的模式(pattern)。
var Alice = new Human(); //alice.energy == 1 //bob.energy == 2
當我們要把這種方式擴大時,事情會變得更有趣。
我們再回顧一下jQuery的faq外掛。如果我們要在這個外掛加入更多的功能會如何?如果我們要透過ajax從伺服器端取得問題所相對應的答案內容時該怎做?想像一下如果faq是另一位作者寫的,而我們要在不改變外掛程式碼的前提下增加新功能(不想把程式變成有分支的)。
我們唯一的選擇就是把整份faq程式碼複製(記得,它只是個單一函式),實際上也就是分支。或者是我們可以呼叫這個外掛然後再多增加一些程式碼。如果可以選擇的話,後者看起來較能省去一些麻煩,程式碼看起來會像這樣:
jQuery.fn.ajaxFaq = function(options) { var settings = jQuery.extend({ //some ajax specific options like the url to request terms from url: '/getfaq.php' definitions: 'dd' }, options); //"this" is the current context; in this case, the elements we want to turn into faq layouts $(this).find(settings.definitions).click(function() { $(this).load(.....); //the logic to load the content from the term }); this.faq(); //call our original faq plug-in });
這作法有些缺點。第一個,我們的faq會多執行一次有時會花很久時間的CSS選擇器;我們沒辦法把已經得到的選擇結果傳給第二次需要用到的場合。第二,我們沒辦法把ajax的程式碼插入在faq外掛程式邏輯的中間(例如要改寫顯示faq的規則)。原本的外掛呼叫了可以用特效展開內容的slideToggle。這將會產生問題,因為在ajax得到完整資料之前,這個效果可能會出錯,這邊除了複製一整份faq程式之外沒有甚麼解決方法。
現在我們再來看看MooTool的Human類別,它包含了isAlive跟energy兩個屬性以及一個eat方法。如果我們要建立一個新版本的Human且添加一些額外的屬性要怎做呢?用MooTools的話,我們可以延伸(extend)這個類別:
var Ninja = new Class({ Extends: Human, initialize: function(name, age, side) { this.side = side; this.parent(name, age); }, energy: 100, attack: function(target) { this.energy = this.energy - 5; target.isAlive = false; } });
你可以看到我們增加了許多功能到子類別裡面。這個子類別所有的屬性都Ninjas是獨有。Ninjas初始時得到energy值為100、side屬性以及可以擊殺其他但是會消耗的energy的attac方法。
var bob = new Human('Bob', 25); var blackNinja = new Ninja('Nin Tendo', 'unknown', 'evil'); //blackNinja.isAlive = true //blackNinja.name = 'Nin Tendo' blackNinja.attack(bob); //bob never had a chance
把程式碼拆解開來看,其中有些有趣的地方值得思考。在Ninja類別裡面有個initialize方法,這個initialize方法會覆蓋掉原本在Human裡面的,但是我們還是會透過this.parent叫用原本的initialize,把兩個原本該有的兩個參數傳過去。更進一步地,我們可以控制新的程式碼要出現在原來程式碼的前面或是後面。我們可以給屬性新的值(像是新的energy值)還可以定義新的功能,想想看這些是不是能在jQuery的faq外掛裡做到。我們可以載入我們自己的ajax然後用滑動效果來展開內容。
MooTools還有另一個模式叫做Mixin。不同於延伸一個類別到其子類別來定義父子關係,你也可以定義一個類別去混合其他類別進而感染(imbue)其他類別的屬性。如這個範例:
var Warrior = new Class({ energy: 100, kills: 0, attack: function(target) { target.isAlive = false; this.energy = this.energy - 5; this.kills++; } });
這裡我們把Ninja不同於Human的特性拆散開來並且放在一個類別裡。這樣我們就可以在Ninja以外的地方重複使用這些程式碼。我們可以讓Ninja染上戰士(warrior)的特質:
var Ninja = new Class({ Extends: Human, Implements: Warrior, //can be an array if you want to implement more than one initialize: function(name, age, side) { this.side = side; this.parent(name, age); } });
Ninja 依然跟之前的功能完全一樣,而Warrior變成我們可以重複利用的部分:
var Samurai = new Class({ Extends: Human, Implements: Warrior, side: 'good' });
現在我們擁有了武士(Samurai)跟忍者(Ninja),不過我們只需要很少的程式碼就可以定義出Samurai跟Ninja。兩者相同的是它們都有戰士的特質,不同的是武士永遠只會是善良的一方,而忍者可以有搖擺的善惡立場。我們只需要花時間寫出Human跟Warrior類別。當我們能在方法叫用時機以及它們彼此之間相互關係上擁有細微的控制,就能寫出不重複的程式碼而得到三種不同類別。每個我們所建立的實體都有自己的狀態,而程式碼本身也具有很好的可讀性。
現在你對MooTools類別的運作方式有個概括的了解了,讓我們看看把之前寫的jQuery的faq寫成MooTools類別,然後延伸出Ajax功能。
var FAQ = new Class({ //Options is another class provided by MooTools Implements: Options, //these are the default options options: { terms: 'dt', definitions: 'dd' }, initialize: function(container, options) { //we store a reference to our container this.container = $(container); //setOptions is a method provided by the Options mixin //it merges the options passed in with the defaults this.setOptions(options); //we store the terms and definitions this.terms = this.container.getElements(this.options.terms); this.definitions = this.container.getElements(this.options.definitions); //we call our attach method //by breaking this into its own method //it makes our class easier to extend this.attach(); }, attach: function(){ //loop through the terms this.terms.each(function(term, index) { //add a click event to each one term.addEvent('click', function(){ //that calls our toggle method for //the current index this.toggle(index); }.bind(this)); }, this); }, toggle: function(index){ //toggle open the definition for the given index this.definitions[index].slide('toggle'); } });
蛤?好多行程式。即使我們把註解拿掉還是有二十幾行。我之前有說明過,我們可以用跟jQuery版本差不多的行數就寫完這個外掛,那為什麼還要搞得這麼長?就是要把它做得更彈性化。要使用這個類別,只要呼叫他的構建式:
var myFAQ = new FAQ(myContainer); //and now we can call methods on it if we want: myFAQ.toggle(2); //toggle the 3rd element
我們可以存取這個實體的方法跟屬性,但是ajax在哪?我們在jQuery版本遇到的問題是我們沒辦法延遲特效的展開時間來等待ajax取得資料完成,在MooTools版本我們沒這困擾了:
FAQ.Ajax = new Class({ //this class inherits the properties of FAQ Extends: FAQ, //it also gets a new option in addition to the other defaults //this one for url, that we're going to append the index of the //term to; in reality we might make this more robust, but for //this example it serves the purpose options: { url: null; }, //we're going to cache the results, so if a section is opened //twice, we won't hit the server for the data indexesLoaded: [], toggle: function(index){ //if we've already loaded the definition if (this.indexesLoaded[index]) { //just call the previous version of toggle this.parent(index); } else { //otherwise, request the data from the server new Request.HTML({ update: this.definitions[index], url: this.options.url + index, //and when the data is loaded, expand the definition onComplete: function(){ this.indexesLoaded[index] = true; this.definitions[index].slide('toggle'); }.bind(this) }).send(); } } });
現在我們有另一個能夠從遠端取得資料的FAQ,整合成了一個可以在資料讀取完之後才展開的新方式(之前在jQuery版做不到的)。而且我們的程式只需要寫多出來的那部分跟其他寥寥數行。這種擴充性允許你建立一整個系列的外掛,可包含了各種不同的變化。同時也意味著你可以沿用別人寫好的外掛,變更你所需要的部分(不用整份程式複製)。這也解釋了為什麼每一種特定的設計模式裡面,你只能找到很少數的MooTools外掛,大多數你所取得的外掛,若不能夠解決你的需求,你都可以自行擴充你所需要的部分。
如同我之前說的,你當然可以用jQuery寫出包含方法與屬性的複雜widgets。大部分的你所寫的這種程式如果是跟DOM無關的邏輯,那麼就會是屬於傳統的JavaScript,但是jQuery沒有提供一個能夠把實體擴充為次類別的系統,也不能幫你用混搭的方式,來使你的程式更容易被重複使用。最後,jQuery的外掛總是被指定到DOM元素中,如果你要寫一個像是處理URL字串的類別,它並沒有一個能記錄這些狀態的架構可用,除非你自己寫一個。
jQuery專注於表述性、快速簡易開發以及DOM上面,而MooTools著重於擴充性、繼承、可讀性、重複使用與易維護性。如果把這兩者放在一把尺的兩個極端,jQuery可視為一種好入門且快速得到結果的東西,不過(以我的經驗)也會讓程式變得難以再利用與維護(不過這真的看你怎麼做,這並不是jQuery本身的問題),而MooTools需要較長的學習時間,得要寫較多的程式碼才能得到你要的結果,但是之後更容易重複使用與維護。
更進一步地說,MooTools並沒有涵括每一個你能想像可能需要的功能,當然jQuery也沒有。兩者都盡量給你簡潔的程式碼,讓你去寫你要的plug-in或是擴充功能。其目的不是提供你所有你有可能需要的功能,而是給你一個工具讓你能夠做出你能想像的任何東西。這就是JavaScript強大的地方所在,也是JavaScript framework強大之處,而這兩個frameworks更是擅長於此。MooTools用一個更全面的角度讓你擁有一個在DOM的範圍中為所欲為的工具,但代價是一個較為陡峭的學習曲線。MooTools的延展性與整體性的方法提供你一個涵蓋jQuery功能的大集合,但是jQuery注重在一個靈活的DOM API,同時又不會阻礙你應用JavaScript原生的繼承模型,如果你想要的話,甚至也可以額外使用像是MooTools這樣的類別系統。
這就是為什麼我說兩者都是很棒的選者,我花很大的利器在凸顯出兩者之間在程式設計哲學上的不同以及它們各自的優缺點。我不知道自己是否有在對MooTools的偏好這個事實上維持中立,但是我希望這些文章是有幫助的。非關於你選擇使用哪個framework,而是你現在更了解這兩個framework了,但願。如果你有充裕的時間,我強烈建議你分別用這兩個framework實作一個網站,然後寫下你自己對兩者的見解,也許你的觀點會出現我所未及之處。