模塊化你的JS代碼
為什么要使用模塊模式?
因為在全局作用域中聲明的變量和函數(shù)都自動成為全局對象Window的屬性,這經(jīng)常會導致命名沖突,還會導致一些非常重要的可維護性難題,全局變量越多,引入錯誤BUG的概率就越大!所以我們應當盡可能少地使用全局變量,模塊化的目的之一就是為了解決該問題的!
零全局變量模式
該模式應用場景較少,通過一個IIFE(立即執(zhí)行的匿名函數(shù)),將所有代碼包裝起來,這樣一來所有的變量、函數(shù)都被隱藏在該函數(shù)內(nèi)部,不會污染全局。
使用情景:
- 當該代碼不會被其它代碼所依賴時;
- 當不需要在運行時不斷的擴展或修改該代碼時;
- 當代碼較短,且無需和其它代碼產(chǎn)生交互時;
單全局變量模式
基本定義
單全局變量模式即只創(chuàng)建一個全局變量(或盡可能少地創(chuàng)建全局變量),且該全局變量的名稱必須是獨一無二的,不會和現(xiàn)在、將來的內(nèi)置API產(chǎn)生沖突,將所有的功能代碼都掛載到這個全局變量上。
它已經(jīng)被廣泛應用于各種流行的類庫中,如:
- YUI定義了唯一的YUI全局對象
- JQuery定義了兩個全局對象,$和JQuery
- Dojo定義了一個dojo全局對象
- Closure定義了一個goog全局對象
例子:
- var Mymodule= {};
- Mymodule.Book = function(){...};
- Mymodule.Book.prototype.getName = function(){....};
- Mymodule.Car = function(){...};
- Mymodule.Car.prototype.getWheels = function(){....};
一個模塊的定義
模塊是一種通用的功能片段,它并沒有創(chuàng)建新的全局變量或命名空間,相反,所有的代碼都存放于一個單函數(shù)中,可以用一個名稱來表示這個模塊,同樣這個模塊可以依賴其他模塊。
- function CoolModule(){
- var something = 'cool';
- var another = [1,2,3];
- function doSomething(){
- console.log( something);
- }
- function doAnother(){
- console.log(another.join('!'));
- }
- return {
- doSomething: doSomething,
- doAnother: doAnother
- };
- }
- var foo = CoolModule();
- foo.doSomething(); //cool
- foo.doAnother(); //1!2!3
這里的CoolModule 就是一個模塊,不過它只是一個函數(shù),這里調(diào)用CoolModule函數(shù)來創(chuàng)建一個模塊的實例foo,此時就形成了閉包(因為CoolModule返回一個對象,其中的一個屬性引用了內(nèi)部函數(shù)),模塊CoolModule返回的對象就是該模塊的公共API(也可以直接返回一個內(nèi)部函數(shù))
所以,模塊模式需要具備兩個必要條件:
- 必須有外部的封閉函數(shù),且該函數(shù)必須至少被調(diào)用一次(每次調(diào)用都會創(chuàng)建一個新的模塊實例),如CoolModule
- 封閉函數(shù)必須至少有一個內(nèi)部函數(shù)被返回,這樣內(nèi)部函數(shù)才能在私有作用域中形成閉包,并且可以訪問或修改私有的狀態(tài)
單例模塊模式的實現(xiàn):
- var foo = ( function CoolModule(){
- ...//代碼同上例
- })();
- foo.doSomething();
- foo.doAnother();
還可以通過在模塊內(nèi)部保留對公共API對象的內(nèi)部引用,這樣就可以在內(nèi)部對模塊實例進行修改,包括添加、刪除方法和屬性
- function CoolModule(){
- var something = 'cool';
- var another = [1,2,3];
- function change() {
- pubicAPI.doSomething = doAnother;
- }
- function doSomething(){
- console.log( something);
- }
- function doAnother(){
- console.log(another.join('!'));
- }
- var pubicAPI = {
- change: change,
- doSomething: doSomething
- };
- return pubicAPI;
- }
- var foo = CoolModule();
- foo.doSomething(); //cool
- foo.change();
- foo.doSomething(); //1!2!3
- var foo1 = CoolModule();
- foo1.doSomething(); //cool
現(xiàn)代的模塊機制
命名空間是簡單的通過在全局變量中添加屬性來表示的功能性分組。
將不同功能按照命名空間進行分組,可以讓你的單全局變量變得井然有序,同時可以讓團隊成員能夠知曉新功能應該在哪個部分中定義,或者去哪個部分查找已有功能。
例如:定義一個全局變量Y,Y.DOM下的所有方法都是和操作DOM相關的,Y.Event下的所有方法都是和事件相關的。
- 常見的用法是為每一個單獨的JS文件創(chuàng)建一個新的全局變量來聲明自己的命名空間;
- 每個文件都需要給一個命名空間掛載功能;這時就需要首先保證該命名空間是已經(jīng)存在的,可以在單全局變量中定義一個方法來處理該任務:該方法在創(chuàng)建新的命名空間時不會對已有的命名空間造成破壞,使用命名空間時也不需要再去判斷它是否存在。
- var MyGolbal = {
- namespace: function (ns) {
- var parts = ns.split('.'),
- obj = this,
- i, len = parts.length;
- for(i=0;i<len;i++){
- if(!obj[parts[i]]){
- obj[parts[i]] = {}
- }
- obj = obj[parts[i]];
- }
- return obj;
- }
- };
- MyGolbal.namespace('Book'); //創(chuàng)建Book
- MyGolbal.Book; //讀取
- MyGolbal.namespace('Car').prototype.getWheel = function(){...}
大多數(shù)模塊依賴加載器或管理器,本質(zhì)上都是將這種模塊定義封裝進一個友好的API
- var MyModules = (function Manager() {
- var modules = {};
- function define(name, deps, impl) {
- for(var i=0; i<deps.length; i++){
- deps[i] = modules[deps[i]];
- }
- modules[name] = impl.apply(impl,deps);
- }
- function get(name) {
- return modules[name];
- }
- return {
- define: define,
- get: get
- };
- })();
以上代碼的核心是modules[name] = impl.apply(impl,deps);,為了模塊的定義引入了包裝函數(shù)(可以傳入任何依賴),并且將模塊的API存儲在一個根據(jù)名字來管理的模塊列表modules對象中;
使用模塊管理器MyModules來管理模塊:
- MyModules.define('bar',[],function () {
- function hello(who) {
- return 'let me introduce: '+who;
- }
- return{
- hello: hello
- };
- });
- MyModules.define('foo',['bar'],function (bar) {
- var hungry = 'hippo';
- function awesome() {
- console.log(bar.hello(hungry).toUpperCase());
- }
- return {
- awesome: awesome
- };
- });
- var foo = MyModules.get('foo');
- foo.awesome();//LET ME INTRODUCE: HIPPO
異步模塊定義(AMD):
- define('my-books', ['dependency1','dependency2'],
- function (dependency1, dependency2) {
- var Books = {};
- Books.author = {author: 'Mr.zakas'};
- return Books; //返回公共接口API
- }
- );
通過調(diào)用全局函數(shù)define(),并給它傳入模塊名字、依賴列表、一個工廠方法,依賴列表加載完成后執(zhí)行這個工廠方法。AMD模塊模式中,每一個依賴都會對應到獨立的參數(shù)傳入到工廠方法里,即每個被命名的依賴最后都會創(chuàng)建一個對象被傳入到工廠方法內(nèi)。模塊可以是匿名的(即可以省略第一個參數(shù)),因為模塊加載器可以根據(jù)JavaScript文件名來當做模塊名字。要使用AMD模塊,需要通過使用與AMD模塊兼容的模塊加載器,如RequireJS、Dojo來加載AMD模塊
- requre(['my-books'] , function(books){
- books.author;
- ...
- }
- )
以上所說的模塊都是是基于函數(shù)的模塊,它并不是一個能被穩(wěn)定識別的模式(編譯器無法識別),它們的API語義只是在運行時才會被考慮進來。因此可以在運行時修改一個模塊的API
未來的模塊機制
ES6為模塊增加了一級語法支持,每個模塊都可以導入其它模塊或模塊的特定API成員,同樣也可以導出自己的API成員;ES6的模塊沒有‘行內(nèi)’格式,必須被定義在獨立的文件中(一個文件一個模塊)ES6的模塊API更加穩(wěn)定,由于編譯器可以識別,在編譯時就檢查對導入的API成員的引用是否真實存在。若不存在,則編譯器會在運行時就拋出‘早期’錯誤,而不會像往常一樣在運行期采用動態(tài)的解決方案;
bar.js
- function hello(who) {
- return 'let me introduce: '+who;
- }
- export hello; //導出API: hello
foo.js
- //導入bar模塊的hello()
- import hello from 'bar';
- var hungry = 'hippo';
- function awesome() {
- console.log(hello(hungry).toUpperCase());
- }
- export awesome;//導出API: awesome
baz.js
- //完整導入foo和bar模塊
- module foo from 'foo';
- module bar from 'bar';
- foo.awesome();
- import可以將一個模塊中的一個或多個API導入到當前作用域中,并分別綁定在一個變量上;
- module會將整個模塊的API導入并綁定到一個變量上;
- export會將當前模塊的一個標識符(變量、函數(shù))導出為公共API;
- 模塊文件中的內(nèi)容會被當做好像包含在作用域閉包中一樣來處理,就和函數(shù)閉包模塊一樣;