JavaScript 模組化編程解析及執行個體

來源:互聯網
上載者:User

模組是任何大型應用程式架構中不可缺少的一部分,模組可以使我們清晰地分離和組織項目中的代碼單元。在項目開發中,通過移除依賴,松耦合可以使應用程式的可維護性更強。與其他傳統程式設計語言不同,在當前JavaScript裡,並沒有提供原生的、有組織性的引入模組方式。本文就來探討一下目前的常見幾種模組化解決方案。

1.對象字面量標記法

對象字面量可以認為是包含一組索引值對的對象,每一對鍵和值由冒號分隔。對象字面量不需要使用new運算子進行執行個體化,在對象的外部也可以給對象添加屬性和方法。樣本如下:

var myModule = {
    myProperty: "jeri",

    // 對象字面量可以包含屬性和方法
    // 例如,可以聲明模組的設定物件
    myConfig: {
        useCaching: true,
        language: "en"
    },

    // 基本方法
    myMethod1: function () {
        console.log("method1");
    },

    // 根據當前配置輸出資訊
    myMethod2: function () {
        console.log("Caching is:" + '(this.myConfig.useCaching) ? "enabled" : "disabled"');
    },

    // 根據當前配置輸出資訊
    myMethod3: function (newConfig) {
        
        if (typeof newConfig === "object") {
            this.myConfig = newConfig;
            console.log(this.myConfig.language);
        }
    }
}
 如上所述,使用對象字面量有助於封裝和組織代碼,然後不同的對象字面量模組再構成複雜的項目。


2.Module模式

Module模式最初定義在傳統的軟體工程中,為類提供私人和公有封裝的方法。在JavaScript中,並不能可以直接聲明類,但我們可以使用閉包來封裝私人的屬性和方法,進而類比類的概念,在JavaScript中實現Module模式。通過這種方式,我們就使得一個單獨的對象擁有公有/私人方法和變數,從而屏蔽來自全域範圍的特殊部分,也就大大降低了變數聲明之間和函式宣告之間衝突的可能性。


var myModule = (function () {

    // 私人變數
    var privateVar = 0;

    // 私人函數
    var privateFun = function (foo) {
        console.log(foo);
    };

    return {
        // 私人變數
        publicVar: "foo",

        // 公有函數
        publicFun: function (arg) {

            // 修改私人變數
            privateVar ++;

            // 傳入bar調用私人方法
            privateFun(arg);
        }
    };
}) ();


如上所示,通過使用閉包我們封裝了私人變數和方法,而只暴露了一個介面供其他部分調用。私人變數(privateVar)和方法(privateFun)被局限於模組的閉包之中,只有通過公有方法才能訪問。該模式除了返回的是一個對象而不是一個函數之外,非常類似於一個立即調用函數運算式,我們可以為返回的對象添加新的屬性和方法,這些新增的屬性和方法對外部調用者來說都是可用的。

Module模式的這種JavaScript實現對於具有物件導向開發經驗的人來說非常簡潔,但其也有自身的缺點和劣勢。

由於我們訪問公有和私人成員的方式不同,當我們想改變可見度時,我們需要修改每一個曾經使用該成員的地方,並不利於維護和升級,耦合度並不理想。而且,在之後新添加的方法裡,我們並不能訪問以前聲明的私人方法和變數,因為閉包只在建立時完成綁定。我們也無法為私人方法建立自動化單元測試,修正私人方法也是極其困難的,我們需要複寫所有與私人方法互動的公有方法,bug修正時工作量會很大。另外,我們也不能輕易的擴充私人方法。


關於指令碼載入器

要討論 AMD 和 CommonJS 模組,我們必然會談及一個顯而易見的話題——指令碼載入器。目前,指令碼載入是為了讓我們能在現今的各種應用中都能使用模組化的 JavaScript 這個目標而服務的。有很多載入器用於 AMD 和 CommonJS方式中的模組載入,比較出名的有RequireJS 和 curl.js。關於指令碼載入器的使用方式和運行機制,大家可以自行瞭解一下。


3.AMD模組

AMD全稱是Asynchronous Module Definition,即非同步模組載入機制。它誕生於使用XHR+eval的Dojo開發經驗,其整體目標是提供模組化的JavaScript解決方案,避免未來的任何解決方案受到過去解決方案缺點的影響。AMD模組格式本身就是對定義模組的建議,其模組和依賴都可以進行非同步載入,而且具有高度的靈活性,清除了代碼和模組之間可能慣有的緊耦合。

關於AMD有兩個非常重要的概念,那就是用於模組定義的define方法和用於處理依賴載入的require方法。
作為一個規範,只需定義其文法API,而不關心其實現。define函數定義如下:

define(
    [module-name?] /*可選*/,
    [array-of-dependencies?] /*可選*/,
    [module-factory-or-object]
);

其中:

    module-name: 模組標識,可以省略。如果沒有這個屬性,則稱為匿名模組。
    array-of-dependencies: 所依賴的模組,可以省略。
    module-factory-or-object: 模組的實現,或者一個JavaScript對象。

具體樣本如下:

define(
    "myModule",
    ["foo", "bar"],

    // 模組定義函數,依賴(foo,bar)作為參數映射到函數上
    function (foo, bar) {
        // 建立模組
        var myModule = {
            myFun: function () {
                console.log("Jeri");
            }
        }

        // 返回定義的模組
        return myModule;
    }
);


require用於載入JavaScript檔案或模組的代碼,擷取依賴。樣本如下:


// foo,bar為外部模組,載入以後的輸出作為回呼函數的參數傳入,以便訪問
requrie(["foo", "bar"], function (foo, bar) {

    // 其他代碼
    foo.doSomething();
});


下面是一個動態載入依賴的樣本:


define(
    function (requrie) {
        var isReady = false,
        foobar;

        requrie(["foo", "bar"], function (foo, bar) {
            isReady = true,
            foobar = foo() + bar();
        });

        // 返回定義的模組
        return {
            isReady: isReady,
            foobar: foobar
        };
    }
);


AMD模組可以使用外掛程式,也就是說當我們載入依賴時,可以載入任意格式的檔案。AMD對於如何完成靈活模組的定義提供了明確的建議,使用AMD編寫模組化的JS代碼,比現有的全域命名空間和<script>標籤解決方案更加簡潔,沒有全域命名空間汙染,在需要的時候也可以消極式載入指令碼。


4.CommonJS模組

CommonJS規範建議指定一個簡單的API來聲明在瀏覽器外部工作的模組。與AMD不同,它試圖包含更廣泛的引人關注的問題,如IO、檔案系統等。

從結構來看,CommonJS模組是JS中可以複用的部分,匯出特定對象,以便可以用於任何依賴代碼。與AMD表現形式不同的是,CommonJS模組並不使用define進行定義。CommonJS模組由兩部分組成:變數exports和require函數。exports包含了一個模組希望其他模組能夠使用的對象,require函數用來匯入其他模組的匯出,也就是用來載入其他模組依賴。樣本如下:
lib.js


// 新定義的模組方法
function log(arg) {
    console.log(arg);
}

// 把方法暴露給其他模組
exports.log = log;


app.js


// ./lib是我們需要的一個依賴
var lib = requrie("./lib");

// 新定義的模組方法
function foo() {
    lib.log("jeri");
}

// 把方法暴露給其他模組
exports.foo = foo;


雖然在瀏覽器端可以使用CommonJS組織模組,但有不少開發人員認為CommonJS更適合於伺服器端開發,因為很多CommonJS API具有面向伺服器的特性,如io、system等。NodeJs使用的就是CommonJS規範。當一個模組可能用於伺服器端時,一些開發人員傾向於選擇CommonJS,其他情況下使用AMD。

AMD模組可以使用外掛程式,也就是說當我們載入依賴時,可以載入任意格式的檔案,並且可以定義更細粒度的東西,如建構函式和函數,但CommonJS模組僅能定義不易使用的對象。在模組的定義和引入方面,二者也有很大的不同。AMD和CommonJS都是非常優秀的模組模式,各自有不同的目標。

    AMD採用採用瀏覽器優先的開發方法,選擇非同步行為和簡化的回溯相容性,但沒有任何的檔案I/O概念。支援對象、函數、建構函式以及其他類型的對象,在瀏覽器中原生運行。
    CommonJS採用伺服器優先的方法,假定同步行為,沒有全域概念負擔,僅將對象作為模組給予支援。CommonJS支援非封裝模組,更接近下一代ES Harmony規範。

5.ES Harmony模組

TC39——負責制定 ECMAScript 文法和語義以及其未來迭代的標準團體,在近幾年一直在密切關注 JavaScript 在大規模開發中的使用方式的演化,而且也敏感地意識到了需要有更好的語言特性來編寫更加模組化的 JS。基於這個原因,目前有提案已經提出了一系列令人振奮的對語言的補充。雖然Harmony還處於建議階段,但我們可以先一覽新的介面特性。


Imports和Exports模組

在ES.next中,已經為模組依賴和模組匯出提供了更加簡潔的方式,那就是import和export。

    import聲明綁定一個模組,作為局部變數匯出,並能被重新命名,以避免名稱衝突。
    export聲明一個外部可見模組的本地綁定,其它模組能夠讀取這些匯出,但無法進行修改。模組可以匯出子模組,但不能匯出再其他地方定義的模組。匯出也是可以重新命名的。

cakes.js


module staff {
    // 指定匯出
    export var baker = {
        bake: function(item) {
            console.log('Woo! I just baked ' + item);
        }
    }
};

module skills {
    export var specialty = "baking";
    export var experience = "5 years";
};

module cakeFactory {
    // 指定依賴項
    import baker from staff;

    // 通過萬用字元匯入所有東西
    import * from skills;

    export var oven = {
        makeCupcake: function(toppings) {
            baker.bake('cupcake', toppings);
        },
        makeMuffin: function(mSize) {
            baker.bake('muffin', size);
        }
    }
};


遠程載入模組

在ES.next裡還建議支援遠程模組載入,樣本如下:


module cakeFactory from 'http://****/cakes.js';

cakeFactory.oven.makeCupcake('sprinkles');

cakeFactory.oven.makeMuffin('large');


模組載入器 API

模組載入器建議一個動態API在嚴格控制的上下問中載入模組。載入器支援的特徵包括用來載入模組的load( url, moduleInstance, error),以及createModule( object, globalModuleReferences)等等。


用於伺服器的類 CommonJS 模組

對於面向伺服器的開發人員來說,在 ES.next 中提出的模組系統並非局限於對瀏覽器端模組的關注。例如下面是一個在伺服器端使用的類CommonJS模組:
File.js


// io/File.js
export function open(path) {
    // ...
};
export function close(hnd) {
    // ...
};


LexicalHandler.js


// compiler/LexicalHandler.js
module file from 'io/File';

import {open, close} from file;
export function scan( in ) {
    try {
        var h = open( in )...
    } finally {
        close(h)
    }
}


app.js

module lexer from 'compiler/LexicalHandler';
module stdlib from '@std';

// ... scan(cmdline[0]) ...

ES Harmony有了很多令人振奮的新功能加入,以求簡化應用程式的開發,並處理依賴管理等問題。然而,至今為止,還沒有形成新的規範,並不能得到眾多瀏覽器的支援。目前,要想使用Harmony文法的最佳選擇是通過transpiler,如Google的Traceur或Esprima。在新規範發布之前,我們還是選擇AMD和CommonJS較為穩妥。


寫在最後

本文論述了幾種模組化編程的方式,它們各有優劣,各有適用情境。希望在以後的編程實踐中,選擇合適的模式,不斷提高代碼的可讀性、可維護性和可擴充性。



深入理解JavaScript 模組模式



模組模式是JavaScript一種常用的編碼模式。這是一般的理解,但也有一些進階應用程式沒有得到很多關注。在本文中,我將回顧基礎知識,瀏覽一些不錯的進階技巧,甚至我認為是原生基礎的。

基礎知識

首先我們開始簡單概述模型模式。三年前Eric Miraglia(YUI)的博文使模型模式眾所周知。如果你已經很熟悉模型模式,可以直接閱讀“進階模式”。

匿名閉包

這是一切成為可能的基礎,也是JavaScript最好的特性。我們將簡單的建立匿名函數,並立即執行。所有函數內部代碼都在閉包(closure)內。它提供了整個應用生命週期的私人和狀態。

(function () {
    // ... all vars and functions are in this scope only
    // still maintains access to all globals
}());

注意匿名函數周圍的()。這是語言的要求。關鍵字function一般認為是函式宣告,包括()就是函數運算式。

引入全域

JavaScript有個特性,稱為隱性全域。使用變數名稱時,解譯器會從範圍向後尋找變數聲明。如果沒找到,變數會被假定入全域(以後可以全域調用)。如果會被分配使用,在還不存在時全域建立它。這意味著在匿名函數裡使用全域變數很簡單。不幸的是,這會導致代碼難以管理,檔案中不容易區分(對人而言)哪個變數是全域的。

幸好,匿名函數還有一個不錯的選擇。全域變數作為參數傳遞給匿名函數。將它們引入我們的代碼中,既更清晰,又比使用隱性全域更快。下面是一個例子:

(function ($, YAHOO) {
    // 當前域有許可權訪問全域jQuery($)和YAHOO
}(jQuery, YAHOO));

模組出口

有時你不只想用全域變數,但你需要先聲明他們(模組的全域調用)。我們用匿名函數的傳回值,很容易輸出他們。這樣做就完成了基本的模組模式。以下是一個完整例子:

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

注意,我們聲明了一個全域模組MODULE,有兩個公開屬性:方法MODULE.moduleMethod和屬性MODULE.moduleProperty。而且,匿名函數的閉包還維持了私人內部狀態。同時學會之上的內容,我們就很容易引入需要的全域變數,和輸出到全域變數。
 
進階模式

對許多使用者而言以上的還不足,我們可以採用以下的模式創造強大的,可擴充的結構。讓我們使用MODULE模組,一個一個繼續。

擴充

模組模式的一個限制是整個模組必須在一個檔案裡。任何人都瞭解長代碼分割到不同檔案的必要。還好,我們有很好的辦法擴充模組。(在擴充檔案)首先我們引入模組(從全域),給他添加屬性,再輸出他。下面是一個例子擴充模組:

var MODULE = (function (my) {
    my.anotherMethod = function () {
        // 此前的MODULE返回my對象作為全域輸出,因此這個匿名函數的參數MODULE就是上面MODULE匿名函數裡的my
    };
 
    return my;
}(MODULE));

我們再次使用var關鍵字以保持一致性,雖然其實沒必要。代碼執行後,模組獲得一個新公開方法MODULE.anotherMethod。擴充檔案沒有影響模組的私人內部狀態。

松耦合擴充

上面的例子需要我們首先建立模組,然後擴充它,這並不總是必要的。提升JavaScript應用效能最好的操作就是非同步載入指令碼。因而我們可以建立靈活多部分的模組,可以將他們無順序載入,以松耦合擴充。每個檔案應有如下的結構:

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

這個模式裡,var語句是必須的,以標記引入時不存在會建立。這意味著你可以像LABjs一樣同時載入所有模組檔案而不被阻塞。

緊耦合擴充

雖然松耦合很不錯,但模組上也有些限制。最重要的,你不能安全的覆寫模組屬性(因為沒有載入順序)。初始化時也無法使用其他檔案定義的模組屬性(但你可以在初始化後運行)。緊耦合擴充意味著一組載入順序,但是允許覆寫。下面是一個例子(擴充最初定義的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));

這種方式也許最不靈活。他可以實現巧妙的組合,但是犧牲了靈活性。正如我寫的,對象的屬性或方法不是拷貝,而是一個對象的兩個引用。修改一個會影響其他。這可能可以保持遞迴複製對象的屬性固定,但無法固定方法,除了帶eval的方法。不過,我已經完整的包含了模組。(其實就是做了一次淺拷貝)。

跨檔案私人狀態

一個模組分割成幾個檔案有一個嚴重缺陷。每個檔案都有自身的私人狀態,且無權訪問別的檔案的私人狀態。這可以修複的。下面是一個松耦合擴充的例子,不同擴充檔案之間保持了私人狀態:

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;
        },//模組載入後,調用以移除對_private的存取權限
        _unseal = my._unseal = my._unseal || function () {
            my._private = _private;
            my._seal = _seal;
            my._unseal = _unseal;
        };//模組載入前,開啟對_private的訪問,以實現擴充部分對私人內容的操作
     
    // permanent access to _private, _seal, and _unseal
     
    return my;
}(MODULE || {}));

任何檔案都可以在本地的變數_private中設定屬性,他會對別的擴充立即生效(即初始化時所有擴充的私人狀態都儲存在_private變數,並被my._private輸出)。模組完全載入了,應用調用MODULE._seal()方法阻止對私人屬性的讀取(幹掉my._private輸出)。如果此後模組又需要擴充,帶有一個私人方法。載入擴充檔案前調用MODULE._unseal()方法(恢複my._private,外部恢複操作許可權)。載入後調用再seal()。

這個模式一直隨我工作至今,我還沒看到別的地方這樣做的。我覺得這個模式很有用,值得寫上。

子模組

最後的進階模式實際上最簡單。有很多好方法建立子模組。和建立父模組是一樣的:

MODULE.sub = (function () {
    var my = {};
    // 就是多一級命名空間
     
    return my;
}());

雖然很簡單,但我還是提一下。子模組有所有正常模組的功能,包括擴充和私人狀態。


總結

大多數進階模式可以互相組合成更多有用的模式。如果要我提出一個複雜應用的設計模式,我會組合松耦合、私人狀態和子模組。

這裡我還沒有涉及效能,不過我有個小建議:模組模式是效能增益的。他簡化了許多,加快代碼下載。松耦合可以無阻塞並行下載,等價於提高下載速度。可能初始化比別的方法慢一點,但值得權衡。只要全域正確的引入,運行效能不會有任何損失,可能還因為局部變數和更少的引用,加快子模組的載入。

最後,一個例子動態載入子模組到父模組(動態建立)中。這裡就不用私人狀態了,其實加上也很簡單。這段代碼允許整個複雜分成的代碼核心及其子模組等平行載入完全。

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!


相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.