我曾經做過js講師,在我的任教過程中,模塊系統一直是學生們的薄弱點。有一個充分的理由可以解釋這個問題:模塊在javascript中有一段奇怪且不穩定的歷史。這篇文章我們將討論這段歷史,並且,你講了解過去的模塊的相關知識,以更好的理解當前模塊的工作原理。
在學習如何在js中創建模塊之前,首先需要明白,模塊是什么以及為什么會存在模塊。環顧你的周邊,你會發現,很多復雜的東西都是有一個個分離的部件組合在一起構成,進而形成一個完整的東西。
以一只手表為例:
可以看到,一只手表由成百上千的內部部件組成,每一個內部部件都有特定的功能和清晰地邊界以方便與其他部件協作。把這些部件組合在一起,就組成了這只完整的手表。我不是一個手表制造業的專家,但是我因為這種方法的優點是非常直觀的。
可復用性
如果你仔細的觀察一些上圖中的構造,你會發現有很多部件都是重復的。由於這種模塊化為中心的設計,手表中的不同功能也可以用到相同的部件。這種可復用部件的能力簡化的工作制造流程,並且提高的利潤。
可組合性
這種設計是可組合性的非常直觀的案例。通過制定每個部件清晰地邊界,能夠很好地組合每一個部件,以創造一個功能齊全的手表。
可利用性(或許有更好的解釋?)
設想一下制造過程,公司不會制造手表,他們只是將這些部件拼接起來以產出一只完整的手表。他們可以自己制作這些部件,也可以將這些部件外包給其他工廠,這不重要,重要的是這些部件組合在一起就是一只完整的手表,而這些部件來自於哪是無關緊要的。
隔離性
明白手表的整個系統是很困難的,因為它由很多小而復雜的,功能專一的部件組成,每個部件都可以單獨考慮,構造和修復。這種隔離性允許人們單獨工作,不會成為彼此的負擔。並且,如果一個部件循環,僅僅需要更換這個部件看,而不是更換這只表。
組織化
組織是每個獨立的擁有清晰邊界的部件為了與其他部件組合的副產品,伴隨着模塊化,自然就會出現這種情況。
隨着手表這樣的結構不斷產出,我們可以越來越清晰地認識到模塊化的好處,那么,如果我們換成軟件領域呢?其實是一樣的。就像手表的設計一樣,軟件也應該被設計,分割成不同的具有特定功能的部件,並且具有為了與其他部件組合的清晰邊界。不過,在軟件中,這種部件被叫做模模塊。到現在為止,模塊給我們的感覺可能與react組件和函數大相徑庭。那模塊到底包含什么呢?
每一個模塊都具有三部分:依賴,代碼內容還有導出。
依賴
當一個模塊需要其他模塊的功能,它可以import
這個模塊作為依賴,例如,無論什么何時,你想創建一個react組件,你只需要import react
模塊,如果你想使用 lodash
,你也只需要impiort lodash
模塊。
code
確定好你的模塊需要的依賴之后,你就可以開始編寫這個模塊
exports
exports
是當前模塊的接口
,引入這個模塊的開發者可以使用你導出的一切功能。
說了這么多概念,下面讓我們來點實際的代碼。
先來看一個react router的例子,方便起見,可以看一下react提供的模塊目錄,在react router中合理的利用模塊,事實證明,在大多數情況下,他們直接映射react組件到模塊,在react項目中分離組件是很有意義的,重新審查上面的手表結構,將部件換成組件同樣有意義。
來看一下MemoryRouter
模塊的代碼,現在不要關心代碼的含義,只需要集中在代碼的結構上。
// imports
import React from 'react';
import { createMemoryHistory } from 'history';
import Router from './Router';
// code
class MemoryRouter extends React.Component {
history = createMemoryHistory(this.props);
render() {
return (
<Router
history={this.history}
children={this.props.children}
/>;
)
}
}
//exports
export default MemoryRouter;
你可以注意到這個模塊的頂部定義了依賴,和一些使當前模塊正產工作的必需的模塊。接下來,可以看到一些代碼。在這個例子中,創建了一個叫做MemoryRouter的新的react組件,最后,在底部定義了對外導出:MemoryRouter,也就是說,任何導入該模塊的模塊都會得到MemoryRouter這個組件。
現在,我們對軟件中的模塊有了一個淺顯的認識,讓我們回顧一些手表設計帶來的好處,在相同設計的軟件中有哪些可以可以直接應用。
可復用性
因為模塊可以在任何需要它的地方import
,所以模塊的復用性很強,如果模塊在程序中用處很多,你可以單獨創建一個包。這個包可以包含一個或多個其他模塊,並且上傳到npm
開源。 reacrt
、lodash
還有jquery
都是可以從npm上下載的npm包。
可組合性
由於模塊定義了導入和導出,所以很容易組合起來,不僅如此。一個軟件好的設計應該是低耦合,模塊增加了代碼的靈活性。
可利用性
npm上有世界上數量最多的免費模塊,超過七十萬個,如果你需要某個功能的包,就去npm上找吧。
隔離性
這里使用手表的描述也是合適的。不在贅述。
組織化
模塊最大的好處也許是組織化了,模塊帶來的分離,正如你所見的,幫助你避免污染全局命名空間,減少命名沖突。
現在你大概了解了模塊的結構和優點。是時候正式構建模塊了。對此我們的方法是非常有條理的。原因是之前提到的,javascript中的模塊有非常奇怪的歷史,即使有更新的方法在javascript中創建模塊,你也會時不時的看到一些老的創建方式。如果模塊從2018年開始,這個可能沒有一點用處,也就是說,我們會回到2010年的模塊
時代。那時,angularjs剛剛發布,jquery還在大范圍使用。大部分公司使用javascript去構建復雜的web應用,而管理這些復雜的工具就是--模塊。
創建模塊的第一個想法可能就是用文件分離代碼。
// users.js
var users = ['Tyler', 'Sarah', 'Dan'];
function getUsers() {
return users;
}
// dom.js
function addUserToDom(name) {
var node = document.createElement('li');
var text = document.createTextNode(name);
node.appendChild(text);
document.getElementById('users').appendChild(node);
}
document.getElementById('submit')
.addEventListener('click', function() {
var input = document.getElementById('input');
addUserToDom(input.value);
input.value = '';
});
var users = window.getUsers();
for (var i = 0; i < users.length; i++) {
addUserToDom(users[i]);
}
<!-- index.html -->
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
這里查看全部源代碼
ok,我們成功的將app分離成不同的功能文件,是不是意味着我們已經實現了模塊?不,絕對沒有。我們做的只不過是分離代碼所在的位置。在js中,只有創建函數才能生成新的作用域。我們未在函數中生命的變量,全都在全局對象上。也就是說,你可以訪問他們通過window
對象。你會注意到我們可以訪問到,這是糟糕的。因為當我們更改一些方法時,其實就是在改變我們整個app。我們沒有分離我們的代碼到模塊,只是在物理位置上分離了代碼。如果剛開始學習javascript,這個結果可能令你驚訝,不過,這可能是你能想到在js中如何實現模塊化的第一個想法。
那么,如果分享分離沒有給我們提供模塊的功能,那我們要怎么做呢?重復強調一下模塊的優點:復用性、組合型、利用性、隔離性還有可組織性。js有沒有原始的特性以供我們創造模塊,以達到上面說的優點?常規函數?當你思考函數的特點,它的特點和模塊優點相似。所以,接下來該怎么做呢?如果我們暴露一個對象來替代直接把整個app暴露在全局對象下,並且命名這個對象為app
,我們可以吧所有我們app需要用到的方法,掛在在這個app
對象下。這樣會防止我們污染全局變量。我們可以在里面放置任何東西,這樣對於其他應用來說依然是不可見得。
// users.js
function usersWrapper () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
}
usersWrapper()
// dom.js
function domWrapper() {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
}
domWrapper()
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users"></ul>
<input
id="input"
type="text"
placeholder="New User">
</input>
<button id="submit">Submit</button>
<script src="app.js"></script>
<script src="users.js"></script>
<script src="dom.js"></script>
</body>
</html>
這里查看全部源代碼
現在你查看window對象,相比於,只有我們的app
對象,和我們的包裹函數:userWrapper
、domWrapper
。更重要的是,app中非常重要的代碼(比如users
)變得不可更改了。因為它不在在全局環境下了。
讓我們更進一步。有沒有辦法可以丟棄包裹函數?我們只是定義了它們,然后立即調用。給他們一個全局命名的唯一原因就是我們之后可以立即調用它們。如果我們沒有給他們全局命名,有沒有辦法直接直接調用沒有名字(匿名)的函數。不賣關子了,當然有了,就是Immediately Invoked Function Expression
,簡寫為IIFE
。
IIFE
它看起來像下面這樣:
(function() {
console.log('Pronounced IF-EE');
})()
注意,這僅僅是一個被小括號()
包起來的匿名函數。
(function() {
console.log('Pronounced IF-EE');
})
然后,就像其他函數一樣,為了調用函數,我們增加了一對小括號在函數而最后。
(function() {
console.log('Pronounced IF-EE');
})()
現在,為了放棄丑陋的包裹函數和干凈的全局命名空間讓我們來使用IIFE
來更新一下代碼。
// users.js
(function () {
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
APP.getUsers = getUsers
})()
// dom.js
(function () {
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = APP.getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
})()
這里查看全部源代碼
么么噠。現在你在查看window對象,你會發現,我們僅僅掛在了一個app對象在上面,他將作為全局方法的命名空間。
這就是IIFE模塊模式。
IIFE模塊模式有什么優點呢?首先,最重要的一點是,我們沒有污染全局命名空間,這避免了變量沖突,並且提供代碼私有性。有利就有弊,我們仍然有一個全局app變量,如果其他框架使用了相同的代碼,我們就有麻煩了。第二點,你可能主要到了html文件中的script的順序,如果順序不對,那么app直接會掛掉。
不過,就算這不是最完美的。我們依然進步了一大塊。我們知道了IIFE模塊模式的優點和缺點。如果我們用我們的標准創建並管理模塊,它有哪些特性呢?
早些時候,我們對模塊分離的第一感覺每個文件都是一個新的模塊。就算這種想法在js中不是開箱就用的。我認為對模塊來說這是一個非常顯著的分離。每個文件就是一個單獨的模塊,然后我們需要一個特性是每個文件(模塊)都能定義自己的導入和導出。並可在其他文件(模塊)中導入。
我們的標准
- 基於文件的模塊
- 明確的導入
- 明確的導出
現在,我們明確了我們想要的標准,讓我們開始開發api。我們需要定義的看起來像是imports
和exports
,從exports開始。為了保證更好理解,任何和module相關的我們都稱之為module
對象。然后,我們想從模塊導出的內容都放在module.exports
上,就像下面這樣:
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
module.exports.getUsers = getUsers
也可以這樣:
var users = ["Tyler", "Sarah", "Dan"]
function getUsers() {
return users
}
module.exports = {
getUsers: getUsers
}
不管有多少個方法,我們都可以添加到exports
對象上:
// users.js
var users = ["Tyler", "Sarah", "Dan"]
module.exports = {
getUsers: function () {
return users
},
sortUsers: function () {
return users.sort()
},
firstUser: function () {
return users[0]
}
}
好了,我們解決了如何從模塊導出,接下來我們需要解決如何導入。同樣一切從簡,首先假設我們有一個叫做require
的函數,它接受一個字符串路徑作為第一個參數,然后返回從這個路徑下導出的所有內容。接着上面的user.js文件,引入的方式像這樣:
var users = require('./users')
users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]
哦耶~ 利用假象的module.exports
和require
語法,我們不僅保留了模塊的所有優點,還擺脫了IIFE模塊模式的缺點。舒服。
看完這個標准,有沒有靈光一現?這tm不就是commonjs嗎?
commonjs小組定義了模塊模式去解決js作用域問題,以確保每個模塊在他們自己的命名空間執行。通過模塊明確導出那些變量來實現,通過其他模塊定義的require來正確工作。
如果你之前使用過node,conmonjs你會很熟悉。使用node,你可以開箱即用的使用require和module.exports語法,不過,瀏覽器並未支持。事實上,就算瀏覽器支持,瀏覽器也不會使用commonjs,因為它不是異步加載模塊。眾所周知,瀏覽器是單線程。異步才是王道。
簡單總結一下,commonjs有兩個問題,首先瀏覽器不支持,第二,瀏覽器就算支持了也會因為commonjs的同步加載造成很糟糕的用戶體驗。如果我們能修復這兩個問題,這也許是一個好的方案。不過,花費很多的時間去考慮研究commonjs是否對瀏覽器足夠友好有沒有意義呢?不管怎么樣,這有一個新的解決方案,它叫做模塊打包器。
模塊打包器
模塊打包器的作用是檢查你的代碼庫。尋找所有的imports和exports,然后解析打包成瀏覽器可以明白的代碼到一個單獨的新文件。而且你不再用小心翼翼的引入所有script,你應該直接引入打包好的那個文件。
app.js ---> |
users.js -> | Bundler | -> bundle.js
dom.js ---> |
所以,模塊打包器到底做了什么捏?這個問題很大,我也不能全部解釋清楚,不過,這有一個通過webpack打包之后的輸出,你可以自己領悟領悟,哈哈。
這里查看所有源代碼,你也可以下載下來,執行 npm install,然后執行webpack
(function(modules) { // webpackBootstrap
// 模塊緩存
var installedModules = {};
// require函數
function __webpack_require__(moduleId) {
// 檢查module是否有緩存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 創建一個module並緩存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 執行module
modules[moduleId].call(
module.exports,
module,
module.exports,
__webpack_require__
);
// 設置module為已load
module.l = true;
// 返回模塊的導出
return module.exports;
}
// 暴露模塊對象
__webpack_require__.m = modules;
// 暴露模塊緩存
__webpack_require__.c = installedModules;
// 定義getter函數
__webpack_require__.d = function(exports, name, getter) {
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(
exports,
name,
{ enumerable: true, get: getter }
);
}
};
// 在導出中定義__esModule
__webpack_require__.r = function(exports) {
if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 創建假的命名空間對象
// mode & 1: value是模塊id,通過它引入
// mode & 2: 合並所有屬性到ns對象上
// mode & 4: ns已經存在時,直接返回
// mode & 8|1: 行為和require一樣
__webpack_require__.t = function(value, mode) {
if(mode & 1) value = __webpack_require__(value);
if(mode & 8) return value;
if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if(mode & 2 && typeof value != 'string')
for(var key in value)
__webpack_require__.d(ns, key, function(key) {
return value[key];
}.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
};
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({
/***/ "./dom.js":
/*!****************!*\
!*** ./dom.js ***!
\****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {
eval(`
var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
function addUserToDOM(name) {\n
const node = document.createElement(\"li\")\n
const text = document.createTextNode(name)\n
node.appendChild(text)\n\n
document.getElementById(\"users\")\n
.appendChild(node)\n}\n\n
document.getElementById(\"submit\")\n
.addEventListener(\"click\", function() {\n
var input = document.getElementById(\"input\")\n
addUserToDOM(input.value)\n\n
input.value = \"\"\n})\n\n
var users = getUsers()\n
for (var i = 0; i < users.length; i++) {\n
addUserToDOM(users[i])\n
}\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),
/***/ "./users.js":
/*!******************!*\
!*** ./users.js ***!
\******************/
/*! no static exports found */
/***/ (function(module, exports) {
eval(`
var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
function getUsers() {\n
return users\n}\n\nmodule.exports = {\n
getUsers: getUsers\n
}\n\n//# sourceURL=webpack:///./users.js?`);})
});
你可以注意到有很多奇奇怪怪的代碼,你可以閱讀注釋來簡單了解一下到底發生了什么。但是,一個很有趣的事是,打包后的代碼用一個IIFE包裹起來了。也就是說,他們使用了IIFE模塊模式得到了一個相對來說最完美的方案。
javascript的未來是一個活生生的,豐滿的語言。TC-31標准委員會,一年內多次討論如何潛在改善提高javascript語言。換言之,模塊是編寫可伸縮性、可維護的js代碼的關鍵特性。在2013年甚至更早之前,這種說法很顯然是不存在的。js需要一種模塊的標准。一種內建的可處理模塊的解決方法,這也拉開了實現js模塊化的序幕。
如你現在所知道的。如果你之前接受過創建js系統模塊的任務,這個模塊最終看起來將是什么樣的?commonjs?每個文件以一種很清晰的方式定義導入和導出,很顯然,這個是重中之重。但是有個問題,commonjs加載模塊是同步的。雖然這對服務端沒有壓力,但是對瀏覽器不是很友好。一個改變是讓commonjs支持異步加載,另一種我們使用語言自己的模塊化,也就是import
和export
。
這次,我們不需要再假想這種實現了,TC-39標准委員會提出了精確的設計和描述,也就是"ES Modules"。下面讓我們以這種標准化的模塊創建javascript模塊。
EM Modules
正如上面所說的,為了指定你要導出的模塊,你需要使用export
關鍵字。
// utils.js
// Not exported
function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}
// Exported
export function first (arr) {
return arr[0]
}
// Exported
export function last (arr) {
return arr[arr.length - 1]
}
有幾種方式可以導入first
和last
方法,一種是導入所有從urils.js
導出的。
import * as utils from './utils'
utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3
如果我們不想導入全部導出呢?在這個例子中,如果你只想引入first
方法,你能使用一種叫做命名導入的辦法(看起來很想解構,但其實不是哈)。
import { first } from './utils'
first([1,2,3]) // 1
還有呢,不僅僅可以指定多個導出,你還可以指定一個default
導出。
// leftpad.js
export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}
當你使用default
導出這種方式,你的導入方式也會發生變化,代替使用*或者使用命名導入,你可以使用import name from './patn'
import leftpad from './leftpad'
現在,如果你有默認導出,也有其他格式的導出怎么辦呢?這不是問題,按照正確的語法寫就可以了,ES Module沒有這種限制。
// utils.js
function once(fn, context) {
var result
return function() {
if(fn) {
result = fn.apply(context || this, arguments)
fn = null
}
return result
}
}
// regular export
export function first (arr) {
return arr[0]
}
// regular export
export function last (arr) {
return arr[arr.length - 1]
}
// default export
export default function leftpad (str, len, ch) {
var pad = '';
while (true) {
if (len & 1) pad += ch;
len >>= 1;
else break;
}
return pad + str;
}
那導入語法看起來是什么樣的?我覺得你可以想象得到。
import leftpad, { first, last } from './utils'
還是挺爽的是吧?leftpad
是默認導出,first
和last
是常規導出。
ES Modules的關鍵點在於,它是js語言的一部分,並且現代瀏覽器已經支持這種寫法了。現在,讓我們回到一開始的app,不過這次我們使用ES Modules來改寫一遍。
這里查看所有源代碼
// users.js
var users = ["Tyler", "Sarah", "Dan"]
export default function getUsers() {
return users
}
// dom.js
import getUsers from './users.js'
function addUserToDOM(name) {
const node = document.createElement("li")
const text = document.createTextNode(name)
node.appendChild(text)
document.getElementById("users")
.appendChild(node)
}
document.getElementById("submit")
.addEventListener("click", function() {
var input = document.getElementById("input")
addUserToDOM(input.value)
input.value = ""
})
var users = getUsers()
for (var i = 0; i < users.length; i++) {
addUserToDOM(users[i])
}
使用IIFE模式,我們需要使用script引入每個js文件。使用commonjs,我們需要使用webpack等打包器處理我們的代碼,然后引入打包后的文件。而ES Modules中,在一些現在瀏覽器中,我們僅僅需要使用script標簽引入我們的未被處理過的入口文件,然后為script標簽增加屬性:typr='module'
。
<!DOCTYPE html>
<html>
<head>
<title>Users</title>
</head>
<body>
<h1>Users</h1>
<ul id="users">
</ul>
<input id="input" type="text" placeholder="New User"></input>
<button id="submit">Submit</button>
<script type=module src='dom.js'></script>
</body>
</html>
死代碼消除
到這里,還有一個commonjs與ES Modules的不同沒有介紹。
commonjs中,你可以在任何地方引入模塊,甚至通過判斷。
if (pastTheFold === true) {
require('./parallax')
}
ES Modules需要靜態解析(參考js詞法解析,也會有提升的效果)的,import語句必須在模塊頂部,也就是說,他不能再判斷語句中或者其他類似的語句中使用。
if (pastTheFold === true) {
import './parallax' // "import' and 'export' may only appear at the top level"
}
這是因為加載器會進行模塊樹的靜態解析。找到那些真正被用到的,丟棄那些未被使用到的。這是一個很大的話題。換句話說,這也是為什么ES Modules希望你聲明import語句在模塊頂部,這樣打包器會更快的解析的你依賴樹,解析完畢,他才會去真正的工作。
對了,其實你可以使用import()
來動態導入。請自行查找。
希望通過這篇文章可以幫到你。
- script標簽上加上
type='module'
的加載模式都是defer
。 - IIFE:匿名函數
- AMD:依賴前置異步模塊加載
- CMD:就近依賴異步模塊加載
- commonjs(cjs):服務端通用的模塊加載
- UMD:不是單獨的標准,是IIFE、AMD(CMD)、commonjs的結合。
- es:js自己的模塊打包
標准 | 變量問題 | 依賴 | 動態/懶 加載 | 靜態分析 |
---|---|---|---|---|
IIFE | ✔ | × | × | × |
AMD | ✔ | ✔ | ✔ | × |
CMD | ✔ | ✔ | ✔ | × |
commonjs | ✔ | ✔ | ✔ | × |
es6 | ✔ | ✔ | ✔ | ✔ |
再強調一點:es6的模塊是值的引用,commonjs是值的拷貝。參考文章