什麼是 ESM(ES6 Modules or JavaScript Modules) 呢?

前言

首先什麼是 ESM 呢?ESM 全名是「ES6 Modules or JavaScript Modules」,因此這一篇會來介紹一下 ESM。

ES6 Modules or JavaScript Modules

原本早期的 JavaScipt 最主要用於網頁上的 DOM 操作,舉凡:特效、互動等,但是在近幾年來講 JavaScript 可以做相當多複雜的事情,例如 SPA、後端環境開發等,那麼因為複雜度不停地提升關係,所以開發者們開始必須考慮如何將複雜的程式模組化。

那麼為什麼要模組化?這邊舉例一個情境。

假設今天有一個網頁,然後共有每兩頁都會需要渲染畫面的資料,因此檔案方別為 app1.js 與 app2.js,而內容假使如下:

(以下是我早期寫的程式碼,醜跟爛請見諒。)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* app1.js */
function updataList(e) {
var select = e.target.value;
listTitle.textContent = select;
var str = '';
for (var i = 0; i < listLen; i++) {
if (select == data[i].Zone) {
str += `<div class="col-md-6 py-2 px-1">
<div class="card">
<div class="card bg-dark text-white text-left">
<img class="card-img-top bg-cover" height="155px" src="` + data[i].Picture1 + `">
<div class="card-img-overlay d-flex justify-content-between align-items-end p-0 px-3" style="background-color: rgba(0, 0, 0, .2)">
<h5 class="card-img-title-lg">` + data[i].Name + `</h5><h5 class="card-img-title-sm">` + data[i].Zone + `</h5>
</div>
</div>
<div class="card-body text-left">
<p class="card-p-text"><i class="far fa-clock fa-clock-time"></i>&nbsp;` + data[i].Opentime + `</p>
<p class="card-p-text"><i class="fas fa-map-marker-alt fa-map-gps"></i>&nbsp;` + data[i].Add + `</p>
<div class="d-flex justify-content-between align-items-end">
<p class="card-p-text"><i class="fas fa-mobile-alt fa-mobile"></i>&nbsp;` + data[i].Tel + `</p>
<p class="card-p-text"><i class="fas fa-tags text-warning"></i>&nbsp;` + data[i].Ticketinfo + `</p>
</div>
</div>
</div>
</div>`
};
};
list.innerHTML = str;
};
/* app2.js */
function updataList(e) {
var select = e.target.value;
listTitle.textContent = select;
var str = '';
for (var i = 0; i < listLen; i++) {
if (select == data[i].Zone) {
str += `<div class="col-md-6 py-2 px-1">
<div class="card">
<div class="card bg-dark text-white text-left">
<img class="card-img-top bg-cover" height="155px" src="` + data[i].Picture1 + `">
<div class="card-img-overlay d-flex justify-content-between align-items-end p-0 px-3" style="background-color: rgba(0, 0, 0, .2)">
<h5 class="card-img-title-lg">` + data[i].Name + `</h5><h5 class="card-img-title-sm">` + data[i].Zone + `</h5>
</div>
</div>
<div class="card-body text-left">
<p class="card-p-text"><i class="far fa-clock fa-clock-time"></i>&nbsp;` + data[i].Opentime + `</p>
<p class="card-p-text"><i class="fas fa-map-marker-alt fa-map-gps"></i>&nbsp;` + data[i].Add + `</p>
<div class="d-flex justify-content-between align-items-end">
<p class="card-p-text"><i class="fas fa-mobile-alt fa-mobile"></i>&nbsp;` + data[i].Tel + `</p>
<p class="card-p-text"><i class="fas fa-tags text-warning"></i>&nbsp;` + data[i].Ticketinfo + `</p>
</div>
</div>
</div>
</div>`
};
};
list.innerHTML = str;
};

我們可以發現相同的程式碼我們必須在兩個檔案間重複撰寫兩次,如果有更多頁面的話,則必須寫更多次。

因此我們就會希望可以將重複性且可以一直利用的程式碼片段抽出來當作模組使用。

NodeJS

那麼在 Node.js 則是走所謂的 CommonJS,最早名稱是 ServerJS,為什麼特別提到 CommonJS 呢?原因在於前面所講的開發者為了找出如何將複雜的程式碼模組化而出現的一套標準,主要是使用 module.exportsrequire

1
2
3
4
5
6
7
8
9
/* exports.js */

module.exports = 'Hello World'

/* main.js */

const hello = require('exports');

console.log(hello); // Hello World

附帶一提 require 語法是同步載入,因此他必須等待 require 執行完畢才會執行後面的程式碼。

回歸正題,ESM 主要的語法是使用 importexport,如果你有使用過 webpack,但這邊的 ESM 介紹並不是講 Webpack 的部分,而是直接在瀏覽器撰寫,而 ESM 主要吸收了 AMD 與 CommonJS 的優點而出現的,因此也被視為 JavaScript 中非常重要的語法。

那麼想要使用的最主要關鍵在於,你必須將你的 <script> 標籤補上 type="module" 否則你是無法使用 ESM 的。

除此之外接下來也會針對 ESM 稍微介紹幾個重點與如何使用。

ESM 預設嚴格模式

ESM 後的程式碼會自動變成 'use strict'(嚴格模式),不論你有沒有使用 'use strict' 必定都會是嚴格模式。

那麼什麼是嚴格模式呢?由於 JavaScript 是一個相當自由的語言,因此如果對於 JavaScript 沒有一定掌握度,那麼在開發上一定會採許多雷,因此嚴格模式就是要避免我們採到這些奇怪的雷,舉例來講,非常常見的就是打錯字

1
2
3
var person;
persom = {};
console.log(persom);

非嚴格模式

在沒有使用嚴格模式時,是不會出現任何錯誤且還可以正常運行,當你使用嚴格模式後就會完全不同

1
2
3
4
"use strict";
var person ;
persom = {};
console.log(persom);

嚴格模式

那麼基本上你已經對於嚴格模式有一定認知,接下來你可以嘗試將以下程式碼貼到 Codepen,是可以發現依然會出現一樣的錯誤訊息,這邊我就不重複貼上圖片了。

1
2
3
4
5
<script type="module">
var person;
persom = {};
console.log(persom);
</script>

作用域獨立

每一個模組都是獨立的作用域,是無法跨作用域取值,這是什麼意思呢?每一個 script type="module" 都會自己的一個空間,你是無法取得另一個空間的變數或方法。

舉例來講,在原本我們這樣寫時是可以跨 script 取值的

1
2
3
4
5
6
7
<script>
var person = 'Hello Ray';
</script>

<script>
console.log(person); // Hello Ray
</script>

當你在宣告變數 script 標籤補上 type="module" 就會完全不同

1
2
3
4
5
6
7
<script type="module">
var person = 'Hello Ray';
</script>

<script>
console.log(person); // ReferenceError: person is not defined
</script>

因此在使用 ESM 的時候,請務必至少知道這兩點問題,否則你可能會一直踩雷。

具名匯出與匯入

接下來介紹一下何謂具名匯出,其實概念與具名函式與匿名函式非常雷同,你在模組化匯出時它是會有名字是一樣的,而寫法前面有講到主要是使用 export,而具名匯出並沒有特別限制,你可以匯出物件、變數或是函式等等:

1
2
3
4
5
6
7
8
9
10
11
12
/* app.js */
export const myName = 'Hello Ray';

export sayHi() {
console.log('Hello Ray');
}

export const obj = {
myName: 'Ray',
}

export const arr = [1, 2, 3];

具名匯出的最大特色在於,你可以一個檔案內有多個模組匯出,而這邊模組匯入則是使用 import,而具名匯出基本上已經有名字了,因此通常會是 import { ... } form './app.js' 的方式匯入:

1
2
3
4
5
6
7
<script type="module">
import { arr, obj, sayHi, myName } from './app.js';
console.log(myName); // 'Hello Ray'
sayHi(); // Hello Ray
console.log(obj.myName); // Ray
console.log(arr[0]); // 0
</script>

如果你不想要一一匯入的話,你也可以全部匯入,但是通常實務上是不太建議這樣做,因為 debug 很困難,這邊要注意以下 * 代表著是全部匯入,並且 as 賦予到一個新的變數叫做 app 上:

1
2
3
4
5
6
7
<script type="module">
import * as app from './app.js';
console.log(app.myName); // 'Hello Ray'
app.sayHi(); // Hello Ray
console.log(app.obj.myName); // Ray
console.log(app.arr[0]); // 0
</script>

當然你也可以使用 as 來重新命名

1
2
3
4
<script type="module">
import { obj as obj2 } from './app.js';
console.log(obj2.myName); // Ray
</script>

預設匯出與匯入

基本上你可以把預設匯出想像成匿名函式,因此你在匯入時必須給予一個變數接收,而預設匯出主要常見寫法是一個物件:

1
2
3
4
5
6
7
8
9
10
11
/* app.js */
export default {
sayHello() {
console.log('Hello Ray');
},
myName: 'Ray',
obj: {
myName: 'Ray',
},
arr: [1, 2, 3],
};

剛剛有提到預設匯出類似匿名函式,因此你在匯入時,必須給予一個名稱接收

1
2
3
4
5
6
7
<script type="module">
import app from './app.js';
console.log(app.myName); // 'Ray'
app.sayHello(); // Hello Ray
console.log(app.obj.myName); // Ray
console.log(app.arr[0]); // 0
</script>

Side Effect 模組

接下來你應該會很好奇,那麼早期的函式庫是怎麼做到匯出匯入的?例如 jQuery?

這個時候就要提到 Side Effect,在原本早期還沒有 ESM 時,基本上就是使用 IIFE (立即執行函式)的技巧來做到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* jQuery.js */
(function(global){
global.$ = {
sayHello() {
console.log('Hello Ray');
},
myName: 'Ray',
obj: {
myName: 'Ray',
},
arr: [1, 2, 3],
}
})(window);

/* app.js */
console.log($.myName); // 'Ray'
$.sayHello(); // Hello Ray
console.log($.obj.myName); // Ray
console.log($.arr[0]); // 0

那麼最後這邊可能會有疑慮,學這個幹嘛?基本上如果你可以理解這些基本運作邏輯,那麼未來看到這些程式碼時或者想要自己開發較複雜的系統,就必定會使用到這些技巧唷。

參考文獻

Liker 讚賞

這篇文章如果對你有幫助,你可以花 30 秒登入 LikeCoin 並點擊下方拍手按鈕(最多五下)免費支持與牡蠣鼓勵我。
或者你可以也可以請我「喝一杯咖啡(Donate)」。

Buy Me A Coffee Buy Me A Coffee

Google AD

撰寫一篇文章其實真的很花時間,如果你願意「關閉 Adblock (廣告阻擋器)」來支持我的話,我會非常感謝你 ヽ(・∀・)ノ