ECMAScript 之前很難寫大程式,主要是因為沒有 Module 概念,常常一個檔案寫兩三千行程式,且大量使用 Global Variable 造成 Side Effect 很難維護。早期會使用 Module Pattern 解決,稍後更有 CommonJS 與 AMD 試圖制定 Module 標準,一直到 ES Module 後,ECMAScript 模組化總算塵埃落定,是重要里程碑。
Version
macOS Catalina 10.15.1
Node 13.2.0
ECMAScript 2015
Why Module ?
在 ES5 時代,scope 只有兩種概念:global 與 function,而沒有如 C# 的 namespace 或 Java 的 package,因此很難將程式碼加以模組化,造成 ECMAScript 很難寫大程式。
Module 須提供兩大功能:
- 將 data 或 function 封裝在 module 內
- 將 interface 暴露在 module 外
ES5 在語言層級並沒有提供以上支援。
Module Pattern
MouseCounterModule.js
let MouseCounterModule = function() {
let numClicks = 0
let handleClick = console.log(++numClicks)
return {
countClick: () => document.addEventListener('click', handleClick)
}
}()
當語言不支援時,第一個會想用的就是 design pattern 自救。
ECMAScript 什麼都是用 function,module 也用 function 就不意外了。
- 將 data 或 function 封裝在 module 內:使用了 closure + IIFE
- 將 interface 暴露在 module 外:return 全新 object
main.js
<script type="text/javascript" src="MouseCounterModule.js"/>
<script type="texty/javascript">
MouseCounterModule.counterClick()
</script>
使用 HTML 載入 module,此時 ECMAScript 的載入順序就很重要,需要自行控制。
AMD
AMD
Asynchronous Module Defintion
針對 browser 所設計的 module 解決方案,使用 asynchronous 方式載入 module
MouseCounterModule.js
define('MouseCounterModule', ['jQuery'], $ => {
let numClicks = 0
let handleClick = () => console.log(++numClicks)
return {
countClick: () =>
$(document).on('click', handleClick)
}
})
define()
為 AMD 所提供 function:
- 第一個 argument:定義 module 的 ID 作為識別
- 第二個 argument:array,傳入其他 module 的 ID
- 第三個 argument:用來建立 module 的 function
除了 define()
外,該寫的 module function 還是要寫。
- 將 data 或 function 封裝在 module 內:在 function 內使用 closure 封裝
- 將 interface 暴露在 module 外:return 全新 object
不必再使用 IIFE,define()
會幫你執行。
main.js
require(['MouseCounterModule'], mouseCounterModule =>
mouseCounterModule.countClick()
)
require()
為 AMD 所提供 function:
- 第一個 argument:相依的外部 module ID
- 第二個 argument:使用 module 的 function
AMD 有以下特色:
- 自動解析 module 的 dependency,不用在乎 module 載入順序
- Module 以 asynchronous 載入,不會 blocking 影響使用者體驗
- 允許一個檔案有多個 module,也就是多個
define()
也因為 AMD 的 asynchronous 特性,特別適合在 browser 使用。
CommonJS
CommonJS
為一般性 ECMAScript 環境所設計的解決方案,Node.js 使用
MouseCounterModule.js
let $ = require('jQuery')
let numClicks = 0
let handleClick = () => console.log(++numClicks)
module.exports = {
countClick: () => $(document).on('click', handleClick)
}
require()
為 CommonJS 所提供的 function,負責載入 module。
將 data 或 function 封裝在 Module 內:
numClicks
與handleClick()
看似 global,但事實上其 scope 只有 module level,不用特別使用 function 與 closure 寫法就能達成封裝 data 與 function將 interface 暴露在 module 外:將全新 object 指定給
module.exports
即可,不需特別 return
main.js
let MouseCounterModule = require('MouseCounterModule.js')
MouseCounterModule.counterClick()
使用 require()
載入 module 後即可使用,也不用搭配 callback。
CommonJS 有以下特色:
- Data 與 function 不需再使用 closure,雖然看起來像 global,但 CommonJS 會封裝在 module 內
- 使用
module.exports
為公開 interface - 一個檔案就是一個 module
- 語法比 AMD 優雅
但 CommonJS 也有幾個缺點:
- 但
require()
為 synchronous,因此適合在 server 端使用 - Browser 並未提供
module
與exports
,因此還要透過 Browserify 作轉換
ES Module
由於 JavaScript 社群存在這兩大 module 標準,TC39 決定融合 AMD 與 CommonJS 的優點制定出 ES Module,至此 JavaScript 有了正式的 Module 規格。
- 學習 CommonJS,一個檔案就是一個 Module
- 學習 CommonJS 簡單優雅的語法
- 學習 AMD 以 asynchronous 載入 module
MouseCounterModule.js
import $ from 'jquery'
let numClicks = 0
let handleClick = () => console.log(++numClicks);
export default {
countClick: () => $(document).on('click', handleClick)
}
import
為 ES6 所提供的 keyword,負責載入 module,可以 synchronous 也可 asynchronous。
export
亦為 ES6 所提供的 keyword,負責暴露 interface 於 module 外。
將 data 或 function 封裝在 module 內:
numClicks
與handleClick()
看似 global,但事實上其 scope 只有 module level,不用特別使用 function 與 closure 寫法就能達成封裝 data 與 function,這點與 CommonJS 一樣將 interface 暴露在 module 外:將全新 object 透過
export
即可,不需特別 return
main.js
import MouseCounterModule from 'MouseCounterModule.js';
MouseCounterModule.counterClick();
使用 import
載入 module 後即可使用,也不用搭配 callback,這點與 CommonJS 一樣。
ES Module 有以下特色:
- 提供
export
與import
兩個 keyword 就解決 - 自動解析 module 的 dependency,不用在乎 module 載入順序
- 語法比 CommonJS 優雅
Conclusion
- CommonJS 雖然主要用在 Node 後端,但現在前端透過 Browserify 或 Webpack 也可使用 CommonJS
- Node 目前也能使用 ES module,唯支援尚未完整,現階段建議使用
babel-node
將 ES module 轉成 CommonJS module - 若要開發前後端都能使用的 package,建議使用 ES module 語法透過
babel-node
轉成 CommonJS module,如此前後端都能使用
Reference
John Resig, Secret of the JavaScript Ninja, 2nd
MDN, export
MDN, import