ECMAScript 之 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
const MouseCounterModule = function() { let numClicks = 0; const handleClick = () => console.log(++numClicks); return { countClick: () => document.addEventListener('click', handleClick); }; }();
當語言不支援時,第一個會想用的就是 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
<script type="text/javascript" src="MouseCounterModule.js"/> <script type="texty/javascript"> MouseCounterModule.counterClick(); </script>
使用 HTML 載入 Dependency Module,此時 JavaScript 的載入順序就很重要,需要自行控制。
AMD
MouseCounterModule.js
define('MouseCounterModule', ['jQuery'], $ => { let numClicks = 0; const handleClick = () => console.log(++numClicks); return { countClick: () => $(document).on('click', handleClick); }; });
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
require(['MouseCounterModule'], mouseCounterModule => mouseCounterModule.countClick(); );
require()
為 AMD 所提供的 function:
- 第一個引數:相依的外部 Module ID
- 第二個引數:使用 Module 的 function
AMD 有以下特色:
define()
也因為 AMD 的 asynchronous 特性,特別適合在 Browser 使用。
CommonJS
CommonJS
為一般性 JavaScript 環境所設計的解決方案,Node.js 使用
MouseCounterModule.js
const $ = require('jQuery'); let numClicks = 0; const handleClick = () => console.log(++numClicks); module.exports = { countClick: () => $(document).on('click', handleClick); };
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
const MouseCounterModule = require('MouseCounterModule.js'); MouseCounterModule.counterClick();
使用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
import $ from 'jquery'; let numClicks = 0; const handleClick = () => console.log(++numClicks); export default { countClick: () => $(document).on('click', handleClick); };
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
import MouseCounterModule from 'MouseCounterModule.js'; MouseCounterModule.counterClick();
使用import
載入 Dependency Module 後即可使用,也不用搭配 Callback function,這點與 CommonJS 一樣。
CommonJS 有以下特色:
-
提供
export
與import
兩個 keyword 就解決 - 語法比 CommonJS 優雅
Definition
你可以將 data (variable、object、function、class) 加以 imprt 與 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
export let x = 2; export const y = 3;
可直接對let
與const
變數加以 export 。
main.js
import { x } from 'my-module' import { y } from 'my-module' console.log(x); console.log(y); // 2 // 3
可對變數分別 import,但 Named Import 要搭配{}
。
Object
my-module.js
let x = 2; const y = 3; export { x, y };
可直接對 object 加以 export。
main.js
import { x, y } from 'my-module' console.log(x); console.log(y); // 2 // 3
可對 object 直接 import,使用 Object Destructing 方式對 object 直接解構。
Function
my-module.js
export function add(x, y) { return x + y; }
將add()
加以 export。
main.js
import { add } from 'my-module' console.log(add(1, 1)); // 2
對 function 加以 Named Import 要加上{}
。
my-module.js
export const add = (x, y) => x + y;
將add()
Arrow Function 加以 export。
main.js
import { add } from 'my-module' console.log(add(1, 1)); // 2
對 Arrow Function 加以 Named Import 也要加上{}
。
Class
my-module.js
export class Counter { constructor(x, y) { this.x = x; this.y = y; } sum() { return this.x + this.y; } }
將Counter
class 加以 export。
main.js
import { Counter } from 'my-module' const counter = new Counter(1, 1); console.log(counter.sum()); // 2
對 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
const x = 2; const y = 3; export default { x, y };
對於 Anonymous Object 可使用 Default Export。
main.js
import MyObject from 'my-module' console.log(MyObject.x); console.log(MyObject.y);
對 Anonymous Object 使用 Default Import,由於 Anonymous Object 本來就沒有名字,要在 Default Import 重新命名。
Default Import 不用加上{}
。
Function
my-module.js
export default function(x, y) { return x + y; }
對於 Anonymous Function 可使用 Default Export。
main.js
import add from 'my-module' console.log(add(1, 1));
對 Anonymous Function 使用 Default Import,由於 Anonymous Function 本來就沒有名字,要在 Default Import 重新命名。
Default Import 不用加上{}
。
my-module.js
export default (x, y) => x + y;
對於 Arrow Function 可使用 Default Export。
main.js
import add from 'my-module' console.log(add(1, 1));
對 Arrow Function 使用 Default Import,由於 Arrow Function 本來就沒有名字,要在 Default Import 重新命名。
Class
my-module.js
export default class { constructor(x, y) { this.x = x; this.y = y; } sum() { return this.x + this.y; } }
對於 Anonymous Class 可使用 Default Export。
main.js
import Counter from 'my-module' const counter = new Counter(1, 1); console.log(counter.sum());
對 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
export const name = 'Sam'; export const add = (x, y) => x + y; export default class { constructor(x, y) { this.x = x; this.y = y; } sum() { return this.x + this.y; } }
name
與add()
為 Named Export,但 Anonymous Class 為 Default Export。
main.js
import { name } from 'my-module' import { add } from 'my-module' import Counter from 'my-module' console.log(name); console.log(add(1, 1)); const counter = new Counter(1, 1); console.log(counter.sum()); // Sam // 2 // 2
Named Export 要搭配 Named Import。
Default Export 則搭配 Default Export。
Import Entire Module
實務上一個 Module 可能有很多 Export,要一個一個 Import 很辛苦,可以將整個 Module 都 Import 進來。
main.js
import * as MyModule from 'my-module' console.log(MyModule.name); console.log(MyModule.add(1, 1)); const counter = new MyModule.default(1, 1); console.log(counter.sum());
對於 Named Export 沒問題,名字會維持原來的名稱。
但對於沒有名稱的 Default Export,會以default
為名稱。
Alias
若對原本 data 名稱覺得不滿意,在 Named Export 或 Named Import 時都可以重新取別名。
my-module.js
const add = (x, y) => x + y; export { add as sum };
在 Named Export 時,已經使用as
將add
取別名為sum
,需搭配{}
。
main.js
import { sum } from 'my-module' console.log(sum(1, 1));
既然已經取別名為sum
,就以sum
為名稱 import 進來。
my-module.js
export const add = (x, y) => x + y;
直接使用 Named Export 將add()
export 出來。
main.js
import { add as sum } from 'my-module' console.log(sum(1, 1));
在 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,ofollow,noindex">Secret of the JavaScript Ninja, 2rd
MDN,export
MDN,import