Module 是 ECMAScript 發展的重要里程碑

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
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
<script type="text/javascript" src="MouseCounterModule.js"/>
<script type="texty/javascript">
MouseCounterModule.counterClick();
</script>

使用 HTML 載入 Dependency Module,此時 JavaScript 的載入順序就很重要,需要自行控制。

AMD


AMD

Asynchronous Module Defintion
針對 Browser 所設計的 Module 解決方案,使用 Asynchronous 方式載入 Module

MouseCounterModule.js

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
require(['MouseCounterModule'], mouseCounterModule =>
mouseCounterModule.countClick();
);

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
2
3
4
5
6
7
8
9
10
11
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 內numClickshandleClick() 看似 Global,但事實上其 scope 只有 Module level,不用特別使用 function 與 Closure 寫法就能達成封裝 data 與 function

  • 將 interface 暴露在 Module 外:將全新 object 指定給 module.exports 即可,不需特別 return

main.js

1
2
3
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 並未提供 moduleexports,因此還要透過 Browserify 作轉換

ES Module


由於 JavaScript 社群存在這兩大 Module 標準,TC39 決定融合 AMD 與 CommonJS 的優點制定出 ES6 Module,至此 JavaScript 有了正式的 Module 規格。

  • 學習 CommonJS,一個檔案就是一個 Module
  • 學習 CommonJS 簡單優雅的語法
  • 學習 AMD 以 Asynchronous 載入 Module

MouseCounterModule.js

1
2
3
4
5
6
7
8
9
10
11
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 內numClickshandleClick() 看似 Global,但事實上其 scope 只有 Module level,不用特別使用 function 與 Closure 寫法就能達成封裝 data 與 function,這點與 CommonJS 一樣

  • 將 interface 暴露在 Module 外:將全新 object 透過 export 即可,不需特別 return

main.js

1
2
3
import MouseCounterModule from 'MouseCounterModule.js';

MouseCounterModule.counterClick();

使用 import 載入 Dependency Module 後即可使用,也不用搭配 Callback function,這點與 CommonJS 一樣。

ES Module 有以下特色:

  • 提供 exportimport 兩個 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
2
export let x = 2;
export const y = 3;

可直接對 letconst 變數加以 export 。

main.js

1
2
3
4
5
6
7
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

1
2
3
4
const x = 2;
const y = 3;

export { x, y };

可直接對 object 加以 export。

main.js

1
2
3
4
5
6
import { x, y } from 'my-module'

console.log(x);
console.log(y);
// 2
// 3

可對 object 直接 import,使用 Destructing Assignment 方式對 object 直接解構。

Function

my-module.js

1
2
3
export function add(x, y) {
return x + y;
}

add() 加以 export。

main.js

1
2
3
4
import { add } from 'my-module'

console.log(add(1, 1));
// 2

對 function 加以 Named Import 要加上 {}

my-module.js

1
export const add = (x, y) => x + y;

add() Arrow Function 加以 export。

main.js

1
2
3
4
import { add } from 'my-module'

console.log(add(1, 1));
// 2

對 Arrow Function 加以 Named Import 也要加上 {}

Class

my-module.js

1
2
3
4
5
6
7
8
9
10
export class Counter {
constructor(x, y) {
this.x = x;
this.y = y;
}

sum() {
return this.x + this.y;
}
}

Counter class 加以 export。

main.js

1
2
3
4
5
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 無法對 varletconst 使用 Default Export。

Object

my-module.js

1
2
3
4
const x = 2;
const y = 3;

export default { x, y };

對於 Anonymous Object 可使用 Default Export。

main.js

1
2
3
4
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

1
2
3
export default function(x, y) {
return x + y;
}

對於 Anonymous Function 可使用 Default Export。

main.js

1
2
3
import add from 'my-module'

console.log(add(1, 1));

對 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
2
3
import add from 'my-module'

console.log(add(1, 1));

對 Arrow Function 使用 Default Import,由於 Arrow Function 本來就沒有名字,要在 Default Import 重新命名。

Class

my-module.js

1
2
3
4
5
6
7
8
9
10
export default class {
constructor(x, y) {
this.x = x;
this.y = y;
}

sum() {
return this.x + this.y;
}
}

對於 Anonymous Class 可使用 Default Export。

main.js

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}
}

nameadd() 為 Named Export,但 Anonymous Class 為 Default Export。

main.js

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
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

1
2
3
const add = (x, y) => x + y;

export { add as sum };

在 Named Export 時,已經使用 asadd 取別名為 sum,需搭配 {}

main.js

1
2
3
import { sum } from 'my-module'

console.log(sum(1, 1));

既然已經取別名為 sum,就以 sum 為名稱 import 進來。

my-module.js

1
export const add = (x, y) => x + y;

直接使用 Named Export 將 add() export 出來。

main.js

1
2
3
import { add as sum } from 'my-module'

console.log(sum(1, 1));

在 Named Import 時才使用 as 取別名亦可。

Conclusion


  • ES6 Module 語法很簡單,只有 exportimport 兩個 keyword
  • ES6 Module 分成 Named Export 與 Default Export,一個 Module 只能有一個 Default Export,但可以有多個 Named Export
  • 可以使用 import * as module ,將整個 Module 都 import 進來
  • exportimport 都可搭配 as 取別名

Reference


John Resig, Secret of the JavaScript Ninja, 2nd
MDN, export
MDN, import

2018-10-04