模塊
在 JavaScript 中,模塊只不過是基於函數某些特性的代碼組織方式。
在《你不知道的 JavaScript》中,給出了模塊模式因具備的兩個必要條件:
- 必須有外部的封閉函數,該函數必須至少被調用一次(每次調用都會創建一個新的模塊實例)。
- 封閉函數必須返回至少一個內部函數,這樣內部函數才能在私有作用域中形成閉包,並且可以訪問或者修改私有的狀態。
從中我們可以看到一個比較重要的一點,從函數調用所返回的只有數據屬性而沒有閉包函數的對象並不是真正的模塊。
你看👀,理解閉包的重要性再次體現出來了。
從以上要求的兩點來看,只要滿足相應的條件,我們很容易寫出一個模塊。
const userModule = ((name = 'module') => {
let id = 1,moduleName = name;
const sayName = () => {
console.log('moduleName: %s', moduleName);
};
const sayId = () => {
console.log('id: %s', id);
};
const changeName = value => {
moduleName = value;
};
const changePublicAPI = () => {
publicAPI.sayIdentification = sayId
};
const publicAPI = {
sayIdentification: sayName,
changeName,
changePublicAPI,
}
return publicAPI;
})();
以上在滿足兩個必要的基礎上轉換成了 IIFE(立即執行函數表達式)。同時可以看出,基於函數的模塊可以在運行時通過內部保留着公共 API 對象的引用,從而對模塊實例進行修改。
模塊機制
模塊的出現也是爲了能夠提高代碼的複用率,方便代碼管理。複用模塊,自然會出現模塊依賴的問題,所以說我們需要一個管理模塊依賴的模塊。
const moduleManage = (() => {
let modules = {};
const define = (name, deps, module) => {
deps = deps.map(item => modules[item])
modules[name] = module(...deps);
};
const exports = (name) => {
return modules[name];
}
return {
define,
exports,
}
})();
moduleManage.define('a', [], () => {
const sayName = name => {
console.log('name: %s', name);
};
return {
sayName,
}
});
moduleManage.define('b', ['a'], (a) => {
let name = 'b';
const sayName = () => {
a.sayName(name)
};
return {
sayName,
}
});
var b = moduleManage.exports('b');
b.sayName();
模塊依賴管理器也依然是個模塊,這裏的實現其實很簡單。modules[name] = module(...deps)
,使用 modules 緩存各個模塊,對於依賴模塊的模塊,則把依賴作爲參數使用。
規範
CommonJS 規範服務於服務端,同步阻塞,在寫法風格上是依賴就近。但是在瀏覽器上,CommonJS 就不好使了,瀏覽器需要從服務器請求數據,下載完成後纔會有下一步的執行。如果採用 CommonJS 的同步方式,指不定什麼時候文件纔會下載完成。
爲了推廣到瀏覽器上,AMD 規範採用異步方式加載模塊。先異步加載模塊,加載完成後就可以在回調中使用依賴模塊了。這樣就保證了在使用依賴時,依賴已經加載完成。AMD 規範是早早地下載,早早地執行,在回調裏 require
的是依賴的引用。在寫法風格上是依賴前置,這種風格已經不同於 CommonJS 了。還有,這裏早早地執行會帶來一個問題,如果存在某個依賴某些條件不成立,導致沒有用上。那麼,這裏的早早地執行豈不是多此一舉了?
CMD 規範是 sea.js 推崇的規範,它採用的也是異步加載模塊的方式,只是在依賴模塊的執行時機上有所不同。在寫法風格上,又迴歸到 CommonJS,依賴就近。sea.js 是早早地下載,延遲執行。
到了 ES6,終於從語法上支持模塊化了,ES6 模塊是編譯時加載,使得在編譯時就能確定模塊的依賴關係,而且在將來服務器和瀏覽器都會支持 ES6 的模塊化方案。