模組入門

August 30, 2022

JavaScript 在模組的發展上,有著一段混亂的歷史,正如〈名稱空間管理〉中看到的,曾經有 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 模組來說,將來其他模組可以使用的,是 maxminsumPIE 這些名稱,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 至目前的模組成為該模組中的名稱,如果想一次從模組中 importexport 的全部名稱,必須有個前置名稱參考至一個物件,而被 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.

模組是靜態的,importexport 必須是在模組的頂層,也就是說,你不能在 if..else 或者是函式中放 importexport,因為靜態分析時並不執行程式碼。

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';

分享到 LinkedIn 分享到 Facebook 分享到 Twitter