ECMAScript 之 ES Module
ES5 很難寫大程式,主要是因為 JavaScript 沒有 Module 概念,常常一個檔案寫兩三千行程式,且大量使用 Global Variable 造成 Side Effect 很難維護。
早期 JavaScript 是使用 Module Pattern 解決,稍後更有 CommonJS 與 AMD 試圖制定 Module 標準,一直到 TC39 出手,在 ECMAScript 2015 定義 Module 後,JavaScript 的模組化總算塵埃落定,是 JavaScript 發展的重要里程碑。
Version
ECMAScript 2015
Why Module ?
在 ES5 時代,Scope 只有兩種概念:Global 與 Function,而沒有如 C# 的 Namespace 或 Java 的 Package,因此很難將程式碼加以模組化,造成 JavaScript 很難寫大程式。
Module 須提供兩大功能:
- 將 data 或 function 封裝在 Module 內
- 將 interface 暴露在 Module 外
ES5 在語言層級並沒有提供以上支援。
Module Pattern
MouseCounterModule.js
1 | const MouseCounterModule = function() { |
當語言不支援時,第一個會想用的就是 Design Pattern 自救。
JavaScript 什麼都是用 function,Module 也用 function 就不意外了。
- 將 data 或 function 封裝在 Module 內:使用了 Closure + IIFE
- 將 interface 暴露在 Module 外:return 全新 object
IIFE
Immediately Invoked Function Expression
定義 function 的同時,也順便執行 function,若配合 Closure,可將 data 封裝在 function 內,避免 data 暴露在 Global Scope
main.js
1 | <script type="text/javascript" src="MouseCounterModule.js"/> |
使用 HTML 載入 Dependency Module,此時 JavaScript 的載入順序就很重要,需要自行控制。
AMD
AMD
Asynchronous Module Defintion
針對 Browser 所設計的 Module 解決方案,使用 Asynchronous 方式載入 Module
MouseCounterModule.js
1 | define('MouseCounterModule', ['jQuery'], $ => { |
define()
為 AMD 所提供的 function:
- 第一個參數:定義 Module 的 ID 作為識別
- 第二個參數:陣列,傳入其他 Dependency Module 的 ID
- 第三個參數:用來建立 Module 的 function
除了 define()
外,該寫的 Module function 還是要寫。
- 將 data 或 function 封裝在 Module 內:在 function 內使用 Closure 封裝
- 將 interface 暴露在 Module 外:return 全新 object
不必再使用 IIFE,define()
會幫你執行。
main.js
1 | require(['MouseCounterModule'], mouseCounterModule => |
require()
為 AMD 所提供的 function:
- 第一個參數:相依的外部 Module ID
- 第二個參數:使用 Module 的 function
AMD 有以下特色:
- 自動解析 Module 的 dependency,不用在乎 JavaScript 載入順序
- Module 以 asynchronous 載入,不會 blocking 影響使用者體驗
- 允許一個檔案有多個 Module,也就是多個
define()
也因為 AMD 的 asynchronous 特性,特別適合在 Browser 使用。
CommonJS
CommonJS
為一般性 JavaScript 環境所設計的解決方案,Node.js 使用
MouseCounterModule.js
1 | const $ = require('jQuery'); |
require()
為 CommonJS 所提供的 function,負責載入 Dependency Module。
將 data 或 function 封裝在 Module 內:
numClicks
與handleClick()
看似 Global,但事實上其 scope 只有 Module level,不用特別使用 function 與 Closure 寫法就能達成封裝 data 與 function將 interface 暴露在 Module 外:將全新 object 指定給
module.exports
即可,不需特別 return
main.js
1 | const MouseCounterModule = require('MouseCounterModule.js'); |
使用 require()
載入 Dependency Module 後即可使用,也不用搭配 Callback function。
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 的優點制定出 ES6 Module,至此 JavaScript 有了正式的 Module 規格。
- 學習 CommonJS,一個檔案就是一個 Module
- 學習 CommonJS 簡單優雅的語法
- 學習 AMD 以 Asynchronous 載入 Module
MouseCounterModule.js
1 | import $ from 'jquery'; |
import
為 ECMAScript 2015 所提供的 keyword,負責載入 Dependency Module,可以 Synchronous 也可 Asynchronous。
export
為 ECMAScript 2015 所提供的 keyword,負責暴露 interface 於 Module 外。
將 data 或 function 封裝在 Module 內:
numClicks
與handleClick()
看似 Global,但事實上其 scope 只有 Module level,不用特別使用 function 與 Closure 寫法就能達成封裝 data 與 function,這點與 CommonJS 一樣將 interface 暴露在 Module 外:將全新 object 透過
export
即可,不需特別 return
main.js
1 | import MouseCounterModule from 'MouseCounterModule.js'; |
使用 import
載入 Dependency Module 後即可使用,也不用搭配 Callback function,這點與 CommonJS 一樣。
ES Module 有以下特色:
- 提供
export
與import
兩個 keyword 就解決 - 語法比 CommonJS 優雅
Definition
你可以將 data (variable、object、function、class) 加以 import 或 export。
Export 分為 Named Export 與 Default Export:
- Named Export:data 必須有名稱
- Default Export:data 沒有名稱 (Anonymous Object、Anonymous Function、Anonymous Class)
- 一個 Module 只能有一個 Default Export,但能有無限多個 Named Export
Default Export 的 data 也可以有名稱,但因為會由 import 決定名稱,所以通常會使 data 沒有名稱
Named Export
Variable
my-module.js
1 | export let x = 2; |
可直接對 let
與 const
變數加以 export 。
main.js
1 | import { x } from 'my-module' |
可對變數分別 import,但 Named Import 要搭配 {}
。
Object
my-module.js
1 | const x = 2; |
可直接對 object 加以 export。
main.js
1 | import { x, y } from 'my-module' |
可對 object 直接 import,使用 Destructing Assignment 方式對 object 直接解構。
Function
my-module.js
1 | export function add(x, y) { |
將 add()
加以 export。
main.js
1 | import { add } from 'my-module' |
對 function 加以 Named Import 要加上 {}
。
my-module.js
1 | export const add = (x, y) => x + y; |
將 add()
Arrow Function 加以 export。
main.js
1 | import { add } from 'my-module' |
對 Arrow Function 加以 Named Import 也要加上 {}
。
Class
my-module.js
1 | export class Counter { |
將 Counter
class 加以 export。
main.js
1 | import { Counter } from 'my-module' |
對 class 加以 Named Import 要加上 {}
。
無論對 variable / object / function / class 加以 Named Export,都會事先明確命名,然後在 Named Import 時都加上
{}
Default Export
Variable
ES 6 無法對 var
、let
與 const
使用 Default Export。
Object
my-module.js
1 | const x = 2; |
對於 Anonymous Object 可使用 Default Export。
main.js
1 | import MyObject from 'my-module' |
對 Anonymous Object 使用 Default Import,由於 Anonymous Object 本來就沒有名字,要在 Default Import 重新命名。
Default Import 不用加上 {}
。
Function
my-module.js
1 | export default function(x, y) { |
對於 Anonymous Function 可使用 Default Export。
main.js
1 | import add from 'my-module' |
對 Anonymous Function 使用 Default Import,由於 Anonymous Function 本來就沒有名字,要在 Default Import 重新命名。
Default Import 不用加上 {}
。
my-module.js
1 | export default (x, y) => x + y; |
對於 Arrow Function 可使用 Default Export。
main.js
1 | import add from 'my-module' |
對 Arrow Function 使用 Default Import,由於 Arrow Function 本來就沒有名字,要在 Default Import 重新命名。
Class
my-module.js
1 | export default class { |
對於 Anonymous Class 可使用 Default Export。
main.js
1 | import Counter from 'my-module' |
對 Anonymous Class 使用 Default Import,由於 Anonymous Class 本來就沒有名字,要在 Default Import 重新命名。
無論對 variable / object / function / class 加以 Default Export,可不用事先明確命名 (當然要事先命名亦可,但沒有太大意義),然後在 Named Import 時不用加上
{}
React 與 Vue 喜歡使用 Default Export,優點是可由 user 自行命名,彈性最高;Angular 則喜歡使用 Named Export,由 Framework 事先命名,優點是整個 community 名稱統一
Named + Default Export
一個 Module 只允許一個 Default Export,但可以有多個 Named Export。
my-module.js
1 | export const name = 'Sam'; |
name
與 add()
為 Named Export,但 Anonymous Class 為 Default Export。
main.js
1 | import { name } from 'my-module' |
Named Export 要搭配 Named Import。
Default Export 則搭配 Default Export。
Import Entire Module
實務上一個 Module 可能有很多 Export,要一個一個 Import 很辛苦,可以將整個 Module 都 Import 進來。
main.js
1 | import * as MyModule from 'my-module' |
對於 Named Export 沒問題,名字會維持原來的名稱。
但對於沒有名稱的 Default Export,會以 default
為名稱。
Alias
若對原本 data 名稱覺得不滿意,在 Named Export 或 Named Import 時都可以重新取別名。
my-module.js
1 | const add = (x, y) => x + y; |
在 Named Export 時,已經使用 as
將 add
取別名為 sum
,需搭配 {}
。
main.js
1 | import { sum } from 'my-module' |
既然已經取別名為 sum
,就以 sum
為名稱 import 進來。
my-module.js
1 | export const add = (x, y) => x + y; |
直接使用 Named Export 將 add()
export 出來。
main.js
1 | import { add as sum } from 'my-module' |
在 Named Import 時才使用 as
取別名亦可。
Conclusion
- ES6 Module 語法很簡單,只有
export
與import
兩個 keyword - ES6 Module 分成 Named Export 與 Default Export,一個 Module 只能有一個 Default Export,但可以有多個 Named Export
- 可以使用
import * as module
,將整個 Module 都 import 進來 export
與import
都可搭配as
取別名
Reference
John Resig, Secret of the JavaScript Ninja, 2nd
MDN, export
MDN, import