模組入門
August 30, 2022JavaScript 在模組的發展上,有著一段混亂的歷史,正如〈名稱空間管理〉中看到的,曾經有 CommonJS、AMD 等模組標準,亦存在著各式的變體,這使得不同標準之間的模組若要互相合作,存在著一定的困難度。
ES6 以後納入了模組規範,就在於試圖解決這類的問題,正如 ES6 納入類別語法,在可以用它解決需求的情況下,應該採用以增加互通性,面對 ES6 以後的模組方案也是如此,在可以用它解決模組需求的情況下,當然是儘可能使用。
由於一些歷史性的原因,現在的 Node.js 支援兩套模組系統,一個是 Node.js 本身從一開始就使用的模組系統,另一個就是對 ES6 模組的支援,如果要在 Node.js 使用 ES6 模組的支援,方式之一是將全部的原始碼副檔案命名為 .mjs(Michael Jackson Script),或是在 package.json 裡設定 type
為 "module"
:
{
"type": "module"
}
前端的話,瀏覽器要載入 ES6 模組,透過 script
標籤,然而 type
屬性的值是 "module"
,這讓瀏覽器知道這會是個 ES6 模組,script
標籤也有歷史性的相關屬性要知道,這之後在談到瀏覽器的 JavaScript 操作時會再說明。
總之,後面的範例暫時就只談 ES6 模組本身,模組檔案的副檔案使用 .js。
export/import
對於 ES6 以後的模組標準,一個 .js 是一個模組檔案,在當中所有的名稱,作用範圍都侷促在 .js 之中,想要可以被使用的名稱,可以使用 export
來公開,例如,定義一個 math.js 作為模組:
function max(a, b) {
return a > b ? a : b;
}
function min(a, b) {
return a < b ? a : b;
}
function sum(...numbers) {
return numbers.reduce((acc, value) => acc + value);
}
const PI = 3.141592653589793;
const E = 2.718281828459045;
let foo = 'foo';
export {max, min, sum, PI, E};
就這個 math
模組來說,將來其他模組可以使用的,是 max
、min
、sum
、PI
與 E
這些名稱,foo
名稱沒有 export
,它僅在 math
中可用,是 math
模組的私有變數。
如果打算在另一個模組中使用 math
模組,可以使用 import from
,就上面的模組定義來說,你必須知道 export
的名稱是什麼,然後指定 import
哪些名稱:
import {max, sum, PI} from './math.js';
console.log(max(10, 5)); // 10
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(PI); // 3.141592653589793
雖然 math
模組中 export
了五個名稱,然而,只有被 import
至目前模組的名稱才能使用,若必要,也可以為被 import
的名稱取個別名:
import {max as maximum} from './math.js';
ES6 希望只有真正需要的名稱,才 import
至目前的模組成為該模組中的名稱,如果想一次從模組中 import
被 export
的全部名稱,必須有個前置名稱參考至一個物件,而被 import
的名稱,都會是該物件上的特性:
import * as math from './math.js';
console.log(math.max(10, 5));
console.log(math.sum(1, 2, 3, 4, 5));
console.log(math.PI);
一個模組也可以在定義名稱時,同時進行 export
:
export function max(a, b) {
return a > b ? a : b;
}
export function min(a, b) {
return a < b ? a : b;
}
export function sum(...numbers) {
return numbers.reduce((acc, value) => acc + value);
}
export const PI = 3.141592653589793;
export const E = 2.718281828459045;
在 export
時,也可以為名稱取別名再 export
:
function max(a, b) {
return a > b ? a : b;
}
function min(a, b) {
return a < b ? a : b;
}
function sum(...numbers) {
return numbers.reduce((acc, value) => acc + value);
}
const PI = 3.141592653589793;
const E = 2.718281828459045;
export {max as maximum, min as minimum, sum, PI, E};
被 import
的名稱,無論是否宣告為 const
,都是不可變動(Immutable),試圖重新指定值給它,會引發 TypeError
:
import {maximum, minimum} from './math.js';
maximum = function() {}; // TypeError: Assignment to constant variable.
模組是靜態的,import
或 export
必須是在模組的頂層,也就是說,你不能在 if..else
或者是函式中放 import
或 export
,因為靜態分析時並不執行程式碼。
export/export default
模組若要公開名稱,可以使用 export
,必須注意的是,為了表示公開的是名稱,必須使用 {}
包含,就算只有一個名稱要公開也是一樣,例如:
let a = 10;
export {a};
這樣的 export
稱為 Named Export,你不可以這麼撰寫:
let a = 10;
export a; // SyntaxError: Unexpected token export
相對地,在 import
時也必須使用 {}
表示要匯入的是名稱,就算只有匯入一個名稱:
import {a} from './foo.js';
export
的是名稱,不是被參考的值,因此若名稱後來被指定了新的值,另一個模組匯入後取得的值也會是新的值,例如,如果從下面這個模組匯入了 a
,那麼值會是 20:
let a = 10;
export {a};
a = 20;
你可能會想要從某個模組匯入名稱之後,在目前的模組再進行公開,例如:
import {a, b} from './foo.js';
export {a, b};
一個更方便的寫法是:
export {a, b} from './foo.js';
若再度公開名稱時必須改名也是可以的:
export {a as x, b as y} from './foo.js';
如果要公開的是全部的名稱,可以使用 *
:
export * from './foo.js';
如果你要匯出一個「值」,可以使用 export default
,例如:
export default 10;
一個模組只能有一個 export default
,另一個模組要匯入這個值的話,必須指定一個變數,例如:
import a from './foo.js';
注意到,這邊使用的是 a
而不是 {a}
。由於 export default
匯出的是一個值,因此像是底下的範例:
let x = 10;
export default x;
x = 20;
實際上被 export default
的是 x
的值 10,因而後續將 x
設為 20,匯入此模組的另一模組,得到的值並不會是 20,一個模組可以同時有多個 export
與一個 export default
。
export default
可以讓模組的客戶端,在不知道模組中匯出了哪個名稱的情況下,就能自訂名稱取用模組功能,例如,export default
一個工廠函式,之後使用該工廠函式來取用模組名稱:
// 模組功能實作
...
// 提供工廠函式
function factory(...) {
....
}
export default factory;
另一模組可以自訂工廠函式名稱,例如:
import XD from './foo.js';
XD('id').html('<b>這只是個示範</b>');
也許你會想到,export default
一個物件,讓該物件作為名稱空間:
export default {
f1 : function(...) {...},
f2 : function(...) {...},
...
};
雖然客戶端最後可以像過去那種,以單一物件作為名稱空間的方式來使用此模組的功能,然而,ES6 並不鼓勵使用這種方式,理由是被 import
的物件無法靜態分析,在必要的時候進行最佳化之類。
export default
的本質上,其實是使用 default
作為公開的名稱,而 default
的值就是 export default
後指定的值,因此,也可以使用底下方式來 import
:
import {default as a} from './foo.js';
因此,如果你匯入了某個模組,想要直接匯出該模組的 default
,可以如下:
export {default} from './math.js';
如果撰寫了底下的程式:
import a from './foo.js';
export {a as x};
那麼可以直接改為:
export {default as x} from './foo.js';
如果撰寫了底下的程式:
import {a} from './foo.js';
export default {a};
那麼可以改為:
export {a as default} from './foo.js';