模組化的開發方式可以提高程式碼複用率,方便進行程式碼的管理。通常一個檔案就是一個模組,有自己的作用域,只向外暴露特定的變數和函式。目前流行的js模組化規範有CommonJS、AMD、CMD以及ES6的模組系統。參見阮一峰老師的文章 module-loader 。
一、CommonJS
Node.js是commonJS規範的主要實踐者,它有四個重要的環境變數為模組化的實現提供支援:module
、exports
、require
、global
。實際使用時,用module.exports
定義當前模組對外輸出的介面(不推薦直接用exports
),用require
載入模組。
// 定義模組math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
module.exports = { //在這裡寫上需要向外暴露的函式、變數
add: add,
basicNum: basicNum
}
// 引用自定義的模組時,引數包含路徑,可省略.js
var math = require('./math');
math.add(2, 5);
// 引用核心模組時,不需要帶路徑
var http = require('http');
http.createService(...).listen(3000);
複製程式碼複製程式碼
commonJS用同步的方式載入模組。在服務端,模組檔案都存在本地磁碟,讀取非常快,所以這樣做不會有問題。但是在瀏覽器端,限於網路原因,更合理的方案是使用非同步載入。
二、AMD和require.js
AMD規範採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。這裡介紹用require.js實現AMD規範的模組化:用require.config()
指定引用路徑等,用define()
定義模組,用require()
載入模組。
首先我們需要引入require.js檔案和一個入口檔案main.js。main.js中配置require.config()
並規定專案中用到的基礎模組。
/** 網頁中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口檔案/主模組 **/
// 首先用config()指定各模組路徑和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //實際路徑為js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 執行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
複製程式碼複製程式碼
引用模組的時候,我們將模組名放在[]
中作為reqiure()
的第一引數;如果我們定義的模組本身也依賴其他模組,那就需要將它們放在[]
中作為define()
的第一引數。
// 定義math.js模組
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 定義一個依賴underscore.js的模組
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})
// 引用模組,將模組放在[]內
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
複製程式碼複製程式碼
三、CMD和sea.js
require.js在申明依賴的模組時會在第一之間載入並執行模組內的程式碼:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等於在最前面宣告並初始化了要用到的所有模組
if (false) {
// 即便沒用到某個模組 b,但 b 還是提前執行了
b.foo()
}
});
複製程式碼複製程式碼
CMD是另一種js模組化方案,它與AMD很類似,不同點在於:AMD 推崇依賴前置、提前執行,CMD推崇依賴就近、延遲執行。此規範其實是在sea.js推廣過程中產生的。
/** AMD寫法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等於在最前面宣告並初始化了要用到的所有模組
a.doSomething();
if (false) {
// 即便沒用到某個模組 b,但 b 還是提前執行了
b.doSomething()
}
});
/** CMD寫法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要時申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定義模組 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 載入模組
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
複製程式碼複製程式碼
四、ES6 Module
ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,旨在成為瀏覽器和伺服器通用的模組解決方案。其模組功能主要由兩個命令構成:export
和import
。export
命令用於規定模組的對外介面,import
命令用於輸入其他模組提供的功能。
/** 定義模組 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
/** 引用模組 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
複製程式碼複製程式碼
如上例所示,使用import
命令的時候,使用者需要知道所要載入的變數名或函式名。其實ES6還提供了export default
命令,為模組指定預設輸出,對應的import
語句不需要使用大括號。這也更趨近於ADM的引用寫法。
/** export default **/
//定義輸出
export default { basicNum, add };
//引入
import math from './math';
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
複製程式碼複製程式碼
ES6的模組不是物件,import
命令會被 JavaScript 引擎靜態分析,在編譯時就引入模組程式碼,而不是在程式碼執行時載入,所以無法實現條件載入。也正因為這個,使得靜態分析成為可能。
五、 ES6 模組與 CommonJS 模組的差異
1. CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
- CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。
- ES6 模組的執行機制與 CommonJS 不一樣。JS 引擎對指令碼靜態分析的時候,遇到模組載入命令
import
,就會生成一個只讀引用。等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import
有點像 Unix 系統的“符號連線”,原始值變了,import
載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。
2. CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。
執行時載入: CommonJS 模組就是物件;即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。
編譯時載入: ES6 模組不是物件,而是通過
export
命令顯式指定輸出的程式碼,import
時採用靜態命令的形式。即在import
時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”。
CommonJS 載入的是一個物件(即module.exports
屬性),該物件只有在指令碼執行完才會生成。而 ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。