深入研究JavaScript的Module模式

Ant發表於2013-06-01

原文出處:http://blog.jobbole.com/40409/


Module模式是常見的JavaScript程式設計模式,一般來說這種模式是很好理解的,但是依然有一些高階的用法沒有得到太多的注意。在這篇文章中我會提到Module模式的基礎知識和一些真正重要的話題,包括一個可能是我原創的。


基礎知識

首先我們要大概瞭解一下Module模式(2007年由YUI的EricMiraglia在部落格中提出),如果你已熟悉 Module 模式,可以跳過本部分,直接閱讀“高階模式”。


匿名函式閉包

匿名函式閉包是JavaScript最棒的特徵,沒有之一,是它讓一切都成為了可能。現在我們來建立一個匿名函式然後立即執行。函式中所有的程式碼都是在一個閉包中執行的,閉包決定了在整個執行過程中這些程式碼的私有性和狀態。

(function () {
    // ... all vars and functions are in this scope only
    // still maintains access to all globals
}());
注意在匿名函式外面的括號。這是由於在JavaScript中以function開頭的語句通常被認為是函式宣告。加上了外面的括號之後則建立的是函式表示式。


全域性匯入

JavaScript有一個特徵叫做隱藏的全域性變數。當一個變數名被使用,編譯器會向上級查詢用var來宣告這個變數的語句。如果沒有找到的話這個變數就被認為是全域性的。如果在賦值的時候這樣使用,就會建立一個全域性的作用域。這意味著在一個匿名的閉包中建立一個全域性變數是十分容易的。不幸的是 ,這將會導致程式碼的難以管理,因為對於程式設計師來說,如果全域性的變數不是在一個檔案中宣告會很不清晰。幸運的是 ,匿名函式給我我們另一個選擇。我們可以將全域性變數通過匿名函式的引數來匯入到我們的程式碼中,這樣更加的快速和整潔。

(function ($, YAHOO) {
// now have access to globals jQuery (as $) and YAHOO in this code
}(jQuery, YAHOO));

Module匯出

有時你並不想要使用全域性變數,但是你想要宣告他們。我們可以很容易通過匿名函式的返回值來匯出他們。關於Module模式的基本內容就這麼多,這裡有一個複雜一點的例子。

var MODULE = (function () {
    var my = {},
    privateVariable = 1;
    function privateMethod() {
        // ...
    }
    my.moduleProperty = 1;
    my.moduleMethod = function () {
        // ...
    };
    return my;
}());

這裡我們宣告瞭一個全域性的module叫做MODULE,有兩個公有屬性:一個叫做MODULE.moduleMethod的方法和一個叫做MODULE.moduleProperty的變數。另外他通過匿名函式的閉包來維持私有的內部狀態,當然我們也可使用前面提到的模式,輕鬆匯入所需的全域性變數。


高階模式

之前提到的內容已經可以滿足很多需求了,但我們可以更深入地研究這種模式來創造一些強力的可擴充的結構。讓我們一點一點,繼續通過這個叫做MODULE的module來學習。


擴充

目前,module模式的一個侷限性就是整個module必須是寫在一個檔案裡面的。每個進行過大規模程式碼開發的人都知道將一個檔案分離成多個檔案的重要性。幸運的是我們有一個很好的方式來擴充modules。首先我們匯入一個module,然後加屬性,最後將它匯出。這裡的這個例子,就是用上面所說的方法來擴充MODULE。

var MODULE = (function (my) {
    my.anotherMethod = function () {
        // added method...
    };
 
    return my;
}(MODULE));

雖然不必要,但是為了一致性 ,我們再次使用var關鍵字。然後程式碼執行,module會增加一個叫做MODULE.anotherMethod的公有方法。這個擴充檔案同樣也維持著它私有的內部狀態和匯入。


鬆擴充

我們上面的那個例子需要我們先建立module,然後在對module進行擴充,這並不是必須的。非同步載入指令碼是提升 Javascript 應用效能的最佳方式之一。。通過鬆擴充,我們建立靈活的,可以以任意順序載入的,分成多個檔案的module。每個檔案的結構大致如下:

var MODULE = (function (my) {
    // add capabilities...
 
    return my;
}(MODULE || {}));

在這種模式下,var語句是必須。如果匯入的module並不存在就會被建立。這意味著你可以用類似於LABjs的工具來並行載入這些module的檔案。


緊擴充

雖然鬆擴充已經很棒了,但是它也給你的module增添了一些侷限。最重要的一點是,你沒有辦法安全的重寫module的屬性,在初始化的時候你也不能使用其他檔案中的module屬性(但是你可以在初始化之後執行中使用)。緊擴充包含了一定的載入順序,但是支援重寫,下面是一個例子(擴充了我們最初的MODULE)。

var MODULE = (function (my) {
    var old_moduleMethod = my.moduleMethod;
 
    my.moduleMethod = function () {
        // method override, has access to old through old_moduleMethod...
    };
 
    return my;
}(MODULE));

這裡我們已經重寫了MODULE.moduleMethod,還按照需求保留了對原始方法的引用。


複製和繼承

var MODULE_TWO = (function (old) {
    var my = {},
        key;
 
    for (key in old) {
        if (old.hasOwnProperty(key)) {
            my[key] = old[key];
        }
    }
 
    var super_moduleMethod = old.moduleMethod;
    my.moduleMethod = function () {
        // override method on the clone, access to super through super_moduleMethod
    };
 
    return my;
}(MODULE));
這種模式可能是最不靈活的選擇。雖然它支援了一些優雅的合併,但是代價是犧牲了靈巧性。在我們寫的程式碼中,那些型別是物件或者函式的屬性不會被複制,只會以一個物件的兩份引用的形式存在。一個改變,另外一個也改變。對於物件來說[g5] ,我們可以通過一個遞迴的克隆操作來解決,但是對於函式是沒有辦法的,除了eval。然而,為了完整性我還是包含了它。


跨檔案的私有狀態

把一個module分成多個檔案有一很大的侷限,就是每一個檔案都在維持自身的私有狀態,而且沒有辦法來獲得其他檔案的私有狀態。這個是可以解決的,下面這個鬆擴充的例子,可以在不同檔案中維持私有狀態。

var MODULE = (function (my) {
    var _private = my._private = my._private || {},
        _seal = my._seal = my._seal || function () {
            delete my._private;
            delete my._seal;
            delete my._unseal;
        },
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };
 
    // permanent access to _private, _seal, and _unseal
 
    return my;
}(MODULE || {}));

每一個檔案可以為它的私有變數_private設定屬性,其他檔案可以立即呼叫。當module載入完畢,程式會呼叫MODULE._seal(),讓外部沒有辦法接觸到內部的   _.private。如果之後module要再次擴充,某一個屬性要改變。在載入新檔案前,每一個檔案都可以呼叫_.unsea(),,在程式碼執行之後再呼叫_.seal。

這個模式在我今天的工作中想到的,我從沒有在其他地方見到過。但是我認為這是一個很有用的模式,值得單獨寫出來。


Sub-modules


最後一個高階模式實際上是最簡單的,有很多建立子module的例子,就像建立一般的module一樣的。

MODULE.sub = (function () {
    var my = {};
    // ...
 
    return my;
}());
雖然這可能是很簡單的,但是我決定這值得被寫進來。子module有一般的module所有優質的特性,包括擴充和私有狀態。

總結

大多數高階模式都可以互相組合來建立更有用的新模式。如果一定要讓我提出一個設計複雜應用的方法的話,我會結合鬆擴充,私有狀態,和子module。

在這裡我沒有提到效能相關的事情,但是我可以說,module模式對於效能的提升有好處。它可以減少程式碼量,這就使得程式碼的載入更迅速。鬆擴充使得並行載入成為可能,這同樣提升的載入速度。初始化的時間可能比其他的方法時間長,但是這多花的時間是值得的。只要全域性變數被正確匯入了執行的時候就不會出問題,在子module中由於對變數的引用鏈變短了可能也會提升速度。

最後,這是一個子module自身動態載入的例子(如果不存在就建立),為了簡介我沒有考慮內部狀態,但是即便考慮它也很簡單。這個模式可以讓複雜,多層次的程式碼並行的載入,包括子module和其他所有的東西。

var UTIL = (function (parent, $) {
    var my = parent.ajax = parent.ajax || {};
 
    my.get = function (url, params, callback) {
        // ok, so I'm cheating a bit 
        return $.getJSON(url, params, callback);
    };
 
    // etc...
 
    return parent;
}(UTIL || {}, jQuery));

我希望這些內容是有用的,請在下面留言來分享你的想法。少年們,努力吧,寫出更好的,更模組化的JavaScript。









相關文章