16. 模組
目錄
請支持這本書:購買(PDF、EPUB、MOBI)捐款
(廣告,請不要封鎖。)

16. 模組



16.1 概觀

JavaScript 很久以前就有模組了。然而,它們是透過函式庫實作,而不是內建在語言中。ES6 是 JavaScript 第一次有內建模組。

ES6 模組儲存在檔案中。每個檔案只有一個模組,每個模組只有一個檔案。您可以使用兩種方式從模組中匯出內容。 這兩種方式可以混合使用,但通常最好分開使用。

16.1.1 多個命名匯出

可以有多個命名匯出

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

您也可以匯入完整的模組

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

16.1.2 單一預設匯出

可以有一個預設匯出。例如,一個函式

//------ myFunc.js ------
export default function () { ··· } // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

或一個類別

//------ MyClass.js ------
export default class { ··· } // no semicolon!

//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

請注意,如果您預設匯出函式或類別(它們是匿名宣告),則結尾沒有分號。

16.1.3 瀏覽器:腳本與模組

  腳本 模組
HTML 元素 <script> <script type="module">
預設模式 非嚴格 嚴格
頂層變數是 全域 模組的區域
頂層的 this window 未定義
執行 同步 非同步
宣告式匯入(import 陳述式)
程式化匯入(基於 Promise 的 API)
檔案副檔名 .js .js

16.2 JavaScript 中的模組

儘管 JavaScript 從未有內建模組,但社群已收斂成一種簡單的模組樣式,這在 ES5 及更早版本的函式庫中受到支援。ES6 也採用了這種樣式

這種模組方法避免了全域變數,唯一全域的項目是模組規格說明。

16.2.1 ECMAScript 5 模組系統

ES5 模組系統在沒有語言明確支援的情況下,能運作得很好,這令人印象深刻。最重要的兩個(且不幸地不相容)標準是

以上只是對 ES5 模組的簡化說明。如果您想要更深入的資料,請參閱 Addy Osmani 的「使用 AMD、CommonJS 和 ES Harmony 編寫模組化 JavaScript」。

16.2.2 ECMAScript 6 模組

ECMAScript 6 模組的目標是建立一個讓 CommonJS 和 AMD 使用者都滿意的格式

由於內建在語言中,讓 ES6 模組能超越 CommonJS 和 AMD(詳細資訊會在稍後說明)

ES6 模組標準分為兩個部分

16.3 ES6 模組基礎

匯出有兩種:具名匯出(每個模組數個)和預設匯出(每個模組一個)。如後文所述,可以同時使用這兩種匯出,但通常最好將它們分開。

16.3.1 具名匯出(每個模組數個)

模組可以透過在宣告前加上關鍵字 export 來匯出多個項目。這些匯出以其名稱區分,稱為具名匯出

//------ lib.js ------
export const sqrt = Math.sqrt;
export function square(x) {
    return x * x;
}
export function diag(x, y) {
    return sqrt(square(x) + square(y));
}

//------ main.js ------
import { square, diag } from 'lib';
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

還有其他方法可以指定具名匯出(後文會說明),但我覺得這個方法相當方便:只要像沒有外部世界一樣撰寫程式碼,然後使用關鍵字標記要匯出的所有內容。

如果您願意,也可以匯入整個模組,並透過屬性表示法來參考其具名匯出

//------ main.js ------
import * as lib from 'lib';
console.log(lib.square(11)); // 121
console.log(lib.diag(4, 3)); // 5

使用 CommonJS 語法撰寫的相同程式碼:一段時間以來,我嘗試了幾種聰明的策略,以減少 Node.js 中模組匯出的重複性。現在我比較喜歡以下簡單但略為冗長的風格,它讓人想起揭露模組模式

//------ lib.js ------
var sqrt = Math.sqrt;
function square(x) {
    return x * x;
}
function diag(x, y) {
    return sqrt(square(x) + square(y));
}
module.exports = {
    sqrt: sqrt,
    square: square,
    diag: diag,
};

//------ main.js ------
var square = require('lib').square;
var diag = require('lib').diag;
console.log(square(11)); // 121
console.log(diag(4, 3)); // 5

16.3.2 預設匯出(每個模組一個)

只匯出單一值的模組在 Node.js 社群中非常受歡迎。但它們在前端開發中也很常見,在前端開發中,您通常會為模型和元件建立類別,每個模組一個類別。ES6 模組可以選擇一個預設匯出,也就是主要的匯出值。預設匯出特別容易匯入。

以下 ECMAScript 6 模組「是」一個單一函式

//------ myFunc.js ------
export default function () {} // no semicolon!

//------ main1.js ------
import myFunc from 'myFunc';
myFunc();

預設匯出為類別的 ECMAScript 6 模組如下所示

//------ MyClass.js ------
export default class {} // no semicolon!

//------ main2.js ------
import MyClass from 'MyClass';
const inst = new MyClass();

預設匯出有兩種樣式

  1. 標記宣告
  2. 直接預設匯出值
16.3.2.1 預設匯出樣式 1:標記宣告

您可以使用關鍵字 export default 為任何函式宣告(或產生器函式宣告)或類別宣告加上前綴,使其成為預設匯出

export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!

您也可以在此情況下省略名稱。這使得預設匯出成為 JavaScript 中唯一具有匿名函式宣告和匿名類別宣告的地方

export default function () {} // no semicolon!
export default class {} // no semicolon!
16.3.2.1.1 為什麼是匿名函式宣告,而不是匿名函式表達式?

當您查看前兩行程式碼時,您會預期 export default 的運算元為表達式。它們僅出於一致性的原因而成為宣告:運算元可以是命名宣告,將其匿名版本解釋為表達式會令人困惑(甚至比引入新類型的宣告更令人困惑)。

如果您希望將運算元解釋為表達式,則需要使用括號

export default (function () {});
export default (class {});
16.3.2.2 預設匯出樣式 2:直接匯出預設值

值是透過表達式產生的

export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };

每個預設匯出都具有以下結構。

export default «expression»;

這等於

const __default__ = «expression»;
export { __default__ as default }; // (A)

A 行中的陳述句是一個匯出子句(在後面的章節中說明)。

16.3.2.2.1 為什麼有兩種預設匯出樣式?

引入了第二種預設匯出樣式,因為如果變數宣告宣告多個變數,則無法將其有意義地轉換為預設匯出

export default const foo = 1, bar = 2, baz = 3; // not legal JavaScript!

foobarbaz 三個變數中哪一個會是預設匯出?

16.3.3 匯入和匯出必須在頂層

正如稍後更詳細地說明的那樣,ES6 模組的結構是靜態的,您不能有條件地匯入或匯出內容。這帶來了許多好處。

此限制透過僅允許在模組的頂層匯入和匯出,在語法上強制執行

if (Math.random()) {
    import 'foo'; // SyntaxError
}

// You can’t even nest `import` and `export`
// inside a simple block:
{
    import 'foo'; // SyntaxError
}

16.3.4 匯入會提升

模組匯入會提升(在內部移至目前範圍的開頭)。因此,您在模組中提到它們的位置並不重要,而且以下程式碼可以在沒有任何問題的情況下執行

foo();

import { foo } from 'my_module';

16.3.5 匯入是匯出的唯讀檢視

ES6 模組的匯入是匯出實體的唯讀檢視。這表示連接到模組主體內宣告的變數會保持運作,如下列程式碼所示。

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

這在底層是如何運作的,將在 後面的章節 中說明。

匯入作為檢視具有下列優點

16.3.6 支援循環依賴

如果 A 和 B 兩個模組彼此 循環依賴,表示 A(可能間接/遞移)匯入 B,而 B 匯入 A。如果可能,應避免循環依賴,它們會導致 A 和 B 緊密耦合 – 它們只能一起使用和演進。

那麼,為什麼要支援循環依賴?偶爾,你無法避開它們,這就是為什麼支援它們是一項重要的功能。後面的章節 有更多資訊。

讓我們看看 CommonJS 和 ECMAScript 6 如何處理循環依賴。

16.3.6.1 CommonJS 中的循環依賴

下列 CommonJS 程式碼正確處理兩個模組 ab 彼此循環依賴。

//------ a.js ------
var b = require('b');
function foo() {
    b.bar();
}
exports.foo = foo;

//------ b.js ------
var a = require('a'); // (i)
function bar() {
    if (Math.random()) {
        a.foo(); // (ii)
    }
}
exports.bar = bar;

如果模組 a 首先被匯入,則在第 i 行,模組 b 會在匯出新增到其中之前取得 a 的匯出物件。因此,b 無法在頂層存取 a.foo,但該屬性會在 a 的執行完成後存在。如果之後呼叫 bar(),則第 ii 行中的方法呼叫會運作。

作為一般規則,請記住,對於循環依賴,你無法在模組的主體中存取匯入。這是現象的本質,而且不會隨著 ECMAScript 6 模組而改變。

CommonJS 方法的限制為

這些限制表示匯出者和匯入者都必須知道循環依賴並明確支援它們。

16.3.6.2 ECMAScript 6 中的循環依賴

ES6 模組自動支援循環依賴。也就是說,它們沒有前一節提到的 CommonJS 模組的兩個限制:預設匯出有效,不合格的名稱匯入也一樣(以下範例中的第 i 和 iii 行)。因此,你可以實作循環依賴彼此的模組,如下所示。

//------ a.js ------
import {bar} from 'b'; // (i)
export function foo() {
    bar(); // (ii)
}

//------ b.js ------
import {foo} from 'a'; // (iii)
export function bar() {
    if (Math.random()) {
        foo(); // (iv)
    }
}

這段程式碼有效,因為如前一節所述,匯入是匯出的檢視。這表示即使是不合格的匯入(例如第 ii 行的 bar 和第 iv 行的 foo)也是指派給原始資料的間接參照。因此,在面對循環依賴時,你透過不合格的匯入或透過模組存取名稱匯出並無差別:這兩種情況都涉及間接參照,而且總是有效。

16.4 詳細的匯入和匯出

16.4.1 匯入樣式

ECMAScript 6 提供多種匯入樣式2

只有兩種方法可以結合這些樣式,而且它們出現的順序是固定的;預設匯出總是排在第一位。

16.4.2 名稱匯出樣式:內嵌與子句

在模組中,您可以使用兩種方式來匯出命名項目。

一方面,您可以使用關鍵字 export 來標記宣告。

export var myVar1 = ···;
export let myVar2 = ···;
export const MY_CONST = ···;

export function myFunc() {
    ···
}
export function* myGeneratorFunc() {
    ···
}
export class MyClass {
    ···
}

另一方面,您可以將所有想要匯出的項目列在模組的結尾(其樣式類似於揭露模組模式)。

const MY_CONST = ···;
function myFunc() {
    ···
}

export { MY_CONST, myFunc };

您也可以使用不同的名稱來匯出項目

export { MY_CONST as FOO, myFunc };

16.4.3 重新匯出

重新匯出表示將另一個模組的匯出項目新增到目前模組的匯出項目中。您可以新增所有其他模組的匯出項目

export * from 'src/other_module';

預設匯出項目會被 export * 忽略3

或者,您可以更具選擇性(在重新命名時為選用)

export { foo, bar } from 'src/other_module';

// Renaming: export other_module’s foo as myFoo
export { foo as myFoo, bar } from 'src/other_module';
16.4.3.1 將重新匯出項目設為預設匯出項目

下列陳述會將另一個模組 foo 的預設匯出項目設為目前模組的預設匯出項目

export { default } from 'foo';

下列陳述會將模組 foo 的命名匯出項目 myFunc 設為目前模組的預設匯出項目

export { myFunc as default } from 'foo';

16.4.4 所有匯出樣式

ECMAScript 6 提供了多種匯出樣式4

16.4.5 在模組中同時具有命名匯出和預設匯出

下列模式在 JavaScript 中非常常見:函式庫是一個單一函式,但透過該函式的屬性提供其他服務。範例包括 jQuery 和 Underscore.js。以下是 Underscore 作為 CommonJS 模組的草圖

//------ underscore.js ------
var _ = function (obj) {
    ···
};
var each = _.each = _.forEach =
    function (obj, iterator, context) {
        ···
    };
module.exports = _;

//------ main.js ------
var _ = require('underscore');
var each = _.each;
···

使用 ES6 範例,函式 _ 是預設匯出,而 eachforEach 是命名匯出。事實證明,您實際上可以同時具有命名匯出和預設匯出。例如,先前的 CommonJS 模組重新寫為 ES6 模組,如下所示

//------ underscore.js ------
export default function (obj) {
    ···
}
export function each(obj, iterator, context) {
    ···
}
export { each as forEach };

//------ main.js ------
import _, { each } from 'underscore';
···

請注意,CommonJS 版本和 ECMAScript 6 版本僅大致相似。後者具有扁平結構,而前者是巢狀的。

16.4.5.1 建議:避免混合預設匯出和命名匯出

我通常建議將兩種匯出類型分開:每個模組,只有一個預設匯出或只有一個命名匯出。

不過,這並不是一個非常強烈的建議;偶爾混合使用這兩種方式是有道理的。一個範例是一個預設匯出的實體模組。對於單元測試,可以透過命名匯出額外提供一些內部元件。

16.4.5.2 預設匯出只是一個命名匯出

預設匯出實際上只是一個命名匯出,其特殊名稱為 default。也就是說,下列兩個陳述式是等效的

import { default as foo } from 'lib';
import foo from 'lib';

類似地,下列兩個模組具有相同的預設匯出

//------ module1.js ------
export default function foo() {} // function declaration!

//------ module2.js ------
function foo() {}
export { foo as default };
16.4.5.3 default:作為匯出名稱可以,但作為變數名稱不行

你不能使用保留字(例如 defaultnew)作為變數名稱,但你可以使用它們作為匯出名稱(你也可以在 ECMAScript 5 中使用它們作為屬性名稱)。如果你想要直接匯入此類命名匯出,你必須將它們重新命名為適當的變數名稱。

這表示 default 只能出現在重新命名匯入的左側

import { default as foo } from 'some_module';

而且它只能出現在重新命名匯出的右側

export { foo as default };

在重新匯出時,as 的兩側都是匯出名稱

export { myFunc as default } from 'foo';
export { default as otherFunc } from 'foo';

// The following two statements are equivalent:
export { default } from 'foo';
export { default as default } from 'foo';

16.5 ECMAScript 6 模組載入器 API

除了用於處理模組的宣告式語法之外,還有一個程式化 API。它允許你

16.5.1 載入器

載入器處理解析模組規格符import-from 結尾的字串 ID)、載入模組等。其建構函式為 Reflect.Loader。每個平台都在全域變數 System系統載入器)中保留一個預設執行個體,它實作其特定類型的模組載入。

16.5.2 載入器方法:匯入模組

您可以透過基於 Promises 的 API 以程式方式匯入模組

System.import('some_module')
.then(some_module => {
    // Use some_module
})
.catch(error => {
    ···
});

System.import() 讓您可以

System.import() 會擷取單一模組,您可以使用 Promise.all() 來匯入多個模組

Promise.all(
    ['module1', 'module2', 'module3']
    .map(x => System.import(x)))
.then(([module1, module2, module3]) => {
    // Use module1, module2, module3
});

16.5.3 更多載入器方法

載入器有更多方法。三個重要的方法為

16.5.4 設定模組載入

模組載入器 API 將有各種掛鉤來設定載入程序。使用案例包括

  1. 匯入時檢查模組(例如,透過 JSLint 或 JSHint)。
  2. 匯入時自動轉譯模組(它們可能包含 CoffeeScript 或 TypeScript 程式碼)。
  3. 使用傳統模組(AMD、Node.js)。

可設定的模組載入是 Node.js 和 CommonJS 受限的領域。

16.6 在瀏覽器中使用 ES6 模組

讓我們看看 ES6 模組如何在瀏覽器中受到支援。

16.6.1 瀏覽器:非同步模組與同步腳本

在瀏覽器中,有兩種不同類型的實體:腳本和模組。它們的語法略有不同,且工作方式也不同。

以下是差異的概觀,詳細資訊將在稍後說明

  腳本 模組
HTML 元素 <script> <script type="module">
預設模式 非嚴格 嚴格
頂層變數是 全域 模組的區域
頂層的 this window 未定義
執行 同步 非同步
宣告式匯入(import 陳述式)
程式化匯入(基於 Promise 的 API)
檔案副檔名 .js .js
16.6.1.1 腳本

腳本是傳統的瀏覽器方式,用於嵌入 JavaScript 並參考外部 JavaScript 檔案。腳本具有 網際網路媒體類型,用作

以下是最重要的值

腳本通常會同步載入或執行。JavaScript 執行緒會停止,直到程式碼載入或執行完畢。

16.6.1.2 模組

為了符合 JavaScript 常見的執行至完成語意,模組的主體必須不中斷地執行。這會為匯入模組留下兩個選項

  1. 在執行主體時,同步載入模組。這是 Node.js 的做法。
  2. 在執行主體之前,非同步載入所有模組。這是處理 AMD 模組的方式。這是瀏覽器的最佳選項,因為模組是透過網際網路載入,且執行不必在載入時暫停。作為附加的優點,此方法允許並行載入多個模組。

ECMAScript 6 提供了兩全其美的方案:Node.js 的同步語法加上 AMD 的非同步載入。為了讓兩者都可行,ES6 模組在語法上比 Node.js 模組不靈活:匯入和匯出必須在頂層進行。這表示它們也不能有條件。此限制允許 ES6 模組載入器靜態分析模組匯入的模組,並在執行其主體之前載入這些模組。

腳本的同步性質會阻止它們成為模組。腳本甚至無法宣告性地匯入模組(如果您想這樣做,則必須使用程式化的模組載入器 API)。

模組可以使用 <script> 元素的新變體從瀏覽器中使用,該變體完全是非同步的

<script type="module">
    import $ from 'lib/jquery';
    var x = 123;

    // The current scope is not global
    console.log('$' in window); // false
    console.log('x' in window); // false

    // `this` is undefined
    console.log(this === undefined); // true
</script>

如你所見,元素有其自己的範圍,而「內部」的變數是屬於該範圍的。請注意,模組程式碼隱含地處於嚴格模式。這是一個好消息,再也不需要 'use strict' 了。

類似於一般的 <script> 元素,<script type="module"> 也可用於載入外部模組。例如,下列標籤透過 main 模組啟動一個網路應用程式(屬性名稱 import 是我的發明,目前還不清楚將採用哪個名稱)。

<script type="module" import="impl/main"></script>

透過自訂 <script> 類型在 HTML 中支援模組的優點,在於可以透過多載(一個函式庫)輕鬆地將該支援帶到舊引擎。最終可能會或可能不會有專門的模組元素(例如 <module>)。

16.6.1.3 模組或指令碼 - 取決於內容

檔案是模組還是指令碼,僅由其如何被匯入或載入決定。大多數模組都有匯入或匯出,因此可以被偵測到。但如果模組既沒有匯入也沒有匯出,那麼它就與指令碼沒有區別。例如

var x = 123;

這段程式碼的語意會根據它是被解釋為模組還是指令碼而有所不同

更實際的範例是一個安裝某些東西的模組,例如全域變數中的多載或全域事件監聽器。這樣的模組既不匯入也不匯出任何東西,並透過空的匯入來啟動

import './my_module';

16.7 詳細資訊:匯入作為匯出的檢視

匯入在 CommonJS 和 ES6 中的工作方式不同

以下各節說明了這表示什麼意思。

16.7.1 在 CommonJS 中,匯入是匯出值的副本

使用 CommonJS(Node.js)模組時,事情會以相對熟悉的方式運作。

如果你將一個值匯入到變數中,則該值會被複製兩次:一次是在匯出時(A 行),另一次是在匯入時(B 行)。

//------ lib.js ------
var counter = 3;
function incCounter() {
    counter++;
}
module.exports = {
    counter: counter, // (A)
    incCounter: incCounter,
};

//------ main1.js ------
var counter = require('./lib').counter; // (B)
var incCounter = require('./lib').incCounter;

// The imported value is a (disconnected) copy of a copy
console.log(counter); // 3
incCounter();
console.log(counter); // 3

// The imported value can be changed
counter++;
console.log(counter); // 4

如果您透過 exports 物件存取值,它仍會在匯出時複製一次

//------ main2.js ------
var lib = require('./lib');

// The imported value is a (disconnected) copy
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 3

// The imported value can be changed
lib.counter++;
console.log(lib.counter); // 4

16.7.2 在 ES6 中,匯入是匯出值的即時唯讀檢視

與 CommonJS 相比,匯入是匯出值的檢視。換句話說,每個匯入都是與匯出資料的即時連線。匯入是唯讀的

下列程式碼示範匯入如何像檢視

//------ lib.js ------
export let counter = 3;
export function incCounter() {
    counter++;
}

//------ main1.js ------
import { counter, incCounter } from './lib';

// The imported value `counter` is live
console.log(counter); // 3
incCounter();
console.log(counter); // 4

// The imported value can’t be changed
counter++; // TypeError

如果您透過星號(*)匯入模組物件,您會得到相同的結果

//------ main2.js ------
import * as lib from './lib';

// The imported value `counter` is live
console.log(lib.counter); // 3
lib.incCounter();
console.log(lib.counter); // 4

// The imported value can’t be changed
lib.counter++; // TypeError

請注意,雖然您無法變更匯入的值,但您可以變更它們所參照的物件。例如

//------ lib.js ------
export let obj = {};

//------ main.js ------
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError
16.7.2.1 為何採用新的匯入方法?

為何要引入如此複雜的匯入機制,而且偏離既定的做法?

根據我的經驗,ES6 匯入運作良好,您很少需要思考底層發生了什麼事。

16.7.3 實作檢視

在底層,匯入如何作為匯出的檢視運作?匯出透過資料結構匯出項目管理。所有匯出項目(重新匯出的項目除外)都有下列兩個名稱

匯入實體後,該實體總是透過指標存取,指標有兩個組成部分模組本機名稱。換句話說,該指標參照模組中的繫結(變數的儲存空間)。

讓我們檢視各種匯出所建立的匯出名稱和區域名稱。下表(改編自 ES6 規格)提供概觀,後續章節有更多詳細資訊。

陳述式 區域名稱 匯出名稱
export {v}; 'v' 'v'
export {v as x}; 'v' 'x'
export const v = 123; 'v' 'v'
export function f() {} 'f' 'f'
export default function f() {} 'f' 'default'
export default function () {} '*default*' 'default'
export default 123; '*default*' 'default'
16.7.3.1 匯出子句
function foo() {}
export { foo };
function foo() {}
export { foo as bar };
16.7.3.2 內嵌匯出

這是內嵌匯出

export function foo() {}

它等同於以下程式碼

function foo() {}
export { foo };

因此,我們有以下名稱

16.7.3.3 預設匯出

有兩種預設匯出

16.7.3.3.1 預設匯出表達式

以下程式碼預設匯出表達式 123 的結果

export default 123;

它等同於

const *default* = 123; // *not* legal JavaScript
export { *default* as default };

如果您預設匯出表達式,您會得到

區域名稱的選擇是為了避免與任何其他區域名稱衝突。

請注意,預設匯出仍會建立繫結。但是,由於 *default* 不是合法的識別碼,您無法從模組內部存取該繫結。

16.7.3.3.2 預設匯出可提升宣告和類別宣告

以下程式碼預設匯出函式宣告

export default function foo() {}

它等同於

function foo() {}
export { foo as default };

名稱為

這表示你可以透過將不同的值指定給 foo,來變更模組中預設輸出的值。

(僅限於) 預設輸出,你也可以省略函式宣告的名稱

export default function () {}

這等於

function *default*() {} // *not* legal JavaScript
export { *default* as default };

名稱為

預設輸出的產生器宣告和類別宣告,其運作方式與預設輸出的函式宣告類似。

16.7.4 規格中的匯入作為檢視

此部分提供 ECMAScript 2015 (ES6) 語言規範的指標。

管理匯入

各種輸出的匯出名稱和本機名稱顯示在「表 42」的「原始碼文字模組記錄」部分。而「靜態語意:ExportEntries」部分有更詳細的說明。你可以看到匯出項目是靜態設定的 (在評估模組之前),評估匯出陳述的說明在「執行時期語意:評估」部分。

16.8 ES6 模組的設計目標

如果你想了解 ECMAScript 6 模組,了解哪些目標影響了其設計會有所幫助。主要的目標有

以下小節說明這些目標。

16.8.1 偏好預設輸出

建議預設輸出「就是」模組的模組語法可能看起來有點奇怪,但如果你考慮到一個主要的設計目標是讓預設輸出盡可能方便,這就說得通了。引用 David Herman

ECMAScript 6 偏好單一/預設匯出樣式,並提供最簡潔的語法來匯入預設值。匯入命名匯出可以,甚至應該更簡潔。

16.8.2 靜態模組結構

當前的 JavaScript 模組格式具有動態結構:匯入和匯出的內容會在執行階段變更。ES6 引入其模組格式的原因之一,是為了啟用靜態結構,這有幾個好處。但在我們深入探討之前,讓我們先了解靜態結構的意義。

這表示您可以在編譯階段(靜態地)決定匯入和匯出,您只需要查看原始碼,而不需要執行它。ES6 在語法上強制執行這一點:您只能在頂層匯入和匯出(絕不能巢狀在條件式陳述式中)。而且匯入和匯出陳述式沒有動態部分(不允許變數等)。

以下是兩個沒有靜態結構的 CommonJS 模組範例。在第一個範例中,您必須執行程式碼才能找出它匯入了什麼

var my_lib;
if (Math.random()) {
    my_lib = require('foo');
} else {
    my_lib = require('bar');
}

在第二個範例中,您必須執行程式碼才能找出它匯出了什麼

if (Math.random()) {
    exports.baz = ···;
}

ECMAScript 6 模組較不靈活,並強制您使用靜態結構。因此,您會獲得幾個好處,如下所述。

16.8.2.1 好處:在捆綁期間消除無用程式碼

在前端開發中,模組通常會處理如下

捆綁的原因是

  1. 為了載入所有模組,需要擷取較少的檔案。
  2. 壓縮捆綁的檔案比壓縮個別檔案稍微有效率。
  3. 在捆綁期間,可以移除未使用的匯出,可能會大幅節省空間。

原因 1 對 HTTP/1 很重要,因為請求檔案的成本相對較高。這將隨著 HTTP/2 而改變,因此這個原因在那裡並不重要。

原因 3 仍會令人信服。這只能透過具有靜態結構的模組格式來達成。

16.8.2.2 好處:精簡捆綁,沒有自訂捆綁格式

模組捆綁器 Rollup 證明 ES6 模組可以有效地結合,因為它們都符合單一範圍(在變更變數名稱以消除名稱衝突後)。這要歸功於 ES6 模組的兩個特性

舉例來說,考慮以下兩個 ES6 模組。

// lib.js
export function foo() {}
export function bar() {}

// main.js
import {foo} from './lib.js';
console.log(foo());

Rollup 可以將這兩個 ES6 模組套件成以下單一 ES6 模組(請注意已移除未使用的匯出 bar

function foo() {}

console.log(foo());

Rollup 方法的另一個好處是套件沒有自訂格式,它只是一個 ES6 模組。

16.8.2.3 好處:更快速的匯入查詢

如果您需要 CommonJS 中的函式庫,您會取得一個物件

var lib = require('lib');
lib.someFunc(); // property lookup

因此,透過 lib.someFunc 存取命名匯出表示您必須執行屬性查詢,這很慢,因為它是動態的。

相反地,如果您在 ES6 中匯入函式庫,您會靜態知道其內容,並可以最佳化存取

import * as lib from 'lib';
lib.someFunc(); // statically resolved
16.8.2.4 好處:變數檢查

使用靜態模組結構,您會永遠靜態知道模組內部任何位置可見哪些變數

這對於檢查給定的識別碼是否拼寫正確有很大的幫助。這種類型的檢查是 JSLint 和 JSHint 等 linter 的熱門功能;在 ECMAScript 6 中,大部分都可以由 JavaScript 引擎執行。

此外,任何命名匯入(例如 lib.foo)的存取也可以靜態檢查。

16.8.2.5 好處:準備好巨集

巨集仍然在 JavaScript 未來的藍圖中。如果 JavaScript 引擎支援巨集,您可以透過函式庫為其新增新的語法。 Sweet.js 是 JavaScript 的實驗性巨集系統。以下是 Sweet.js 網站的範例:類別的巨集。

// Define the macro
macro class {
    rule {
        $className {
                constructor $cparams $cbody
                $($mname $mparams $mbody) ...
        }
    } => {
        function $className $cparams $cbody
        $($className.prototype.$mname
            = function $mname $mparams $mbody; ) ...
    }
}

// Use the macro
class Person {
    constructor(name) {
        this.name = name;
    }
    say(msg) {
        console.log(this.name + " says: " + msg);
    }
}
var bob = new Person("Bob");
bob.say("Macros are sweet!");

對於巨集,JavaScript 引擎在編譯前會執行預處理步驟:如果剖析器產生的符號串流中的符號序列與巨集的模式部分相符,則會以巨集主體產生的符號取代。預處理步驟僅在您能夠靜態找出巨集定義時才有效。因此,如果您想透過模組匯入巨集,則它們必須有靜態結構。

16.8.2.6 好處:準備好類型

靜態類型檢查會施加類似巨集的限制:它只能在靜態找到類型定義時執行。同樣地,類型只能從具有靜態結構的模組匯入。

類型之所以有吸引力,是因為它們可以在 JavaScript 中啟用靜態類型化的快速方言,其中可以撰寫對效能至關重要的程式碼。其中一種方言是 低階 JavaScript (LLJS)。

16.8.2.7 好處:支援其他語言

如果您想支援將具有巨集和靜態類型的語言編譯成 JavaScript,則 JavaScript 的模組應具有靜態結構,原因如前兩節所述。

16.8.2.8 本節來源

16.8.3 同時支援同步和非同步載入

ECMAScript 6 模組必須獨立於引擎是否同步載入模組(例如在伺服器上)或非同步載入(例如在瀏覽器中)。其語法非常適合同步載入,非同步載入則由其靜態結構啟用:由於您可以靜態地確定所有匯入,因此可以在評估模組主體之前載入它們(類似於 AMD 模組)。

16.8.4 支援模組之間的循環相依性

支援循環相依性是 ES6 模組的主要目標。原因如下

循環相依性並非本質上邪惡。特別是對於物件,有時你甚至會想要這種相依性。例如,在某些樹狀結構(例如 DOM 文件)中,父層會參考子層,而子層會參考回父層。在函式庫中,你通常可以透過謹慎的設計來避免循環相依性。然而,在大型系統中,它們可能會發生,特別是在重構期間。如果模組系統支援循環相依性,這時就會非常有用,因為在重構時系統不會中斷。

Node.js 文件承認循環相依性的重要性,而 Rob Sayre 提供了額外的證據

資料點:我曾經為 Firefox 實作一個類似 [ECMAScript 6 模組] 的系統。我在發布後 3 週就被要求支援循環相依性。

Alex Fritze 發明且我參與開發的系統並不完美,而且語法也不太美觀。但 它在 7 年後仍然被使用,所以它一定做對了某些事。

16.9 常見問題:模組

16.9.1 我可以使用變數來指定要從哪個模組匯入嗎?

import 陳述式完全是靜態的:其模組指定符始終是固定的。如果你想要動態地決定要載入哪個模組,你需要使用 程式化載入器 API

const moduleSpecifier = 'module_' + Math.random();
System.import(moduleSpecifier)
.then(the_module => {
    // Use the_module
})

16.9.2 我可以條件式或依需求匯入模組嗎?

匯入陳述式必須始終位於模組的最上層。這表示你無法將它們巢狀在 if 陳述式、函式等內部。因此,如果你想要條件式或依需求載入模組,你必須使用 程式化載入器 API

if (Math.random()) {
    System.import('some_module')
    .then(some_module => {
        // Use some_module
    })
}

16.9.3 我可以在 import 陳述式中使用變數嗎?

不行,你不能。請記住,匯入的內容不能依賴於任何在執行時期計算的內容。因此

// Illegal syntax:
import foo from 'some_module'+SUFFIX;

16.9.4 我可以在 import 陳述式中使用解構嗎?

不行,你不能。 import 陳述式只看起來像解構,但完全不同(靜態、匯入是檢視等)。

因此,你不能在 ES6 中執行類似這樣的操作

// Illegal syntax:
import { foo: { bar } } from 'some_module';

16.9.5 命名匯出有必要嗎?為什麼不預設匯出物件?

你可能會想 - 如果我們可以簡單地預設匯出物件(就像在 CommonJS 中),為什麼我們需要命名匯出?答案是,你無法透過物件強制靜態結構,並失去所有相關的優點(這些優點已在 本章節 中說明)。

16.9.6 我可以 eval() 模組的程式碼嗎?

不行,你不能。模組對於 eval() 來說是太高階的建構。 模組載入器 API 提供了從字串建立模組的方法。在語法上,eval() 接受指令碼(不允許 importexport),而不是模組。

16.10 ECMAScript 6 模組的優點

乍看之下,將模組建置到 ECMAScript 6 中似乎是一個無聊的功能 - 畢竟,我們已經有幾個好的模組系統。但 ECMAScript 6 模組有幾個新功能

ES6 模組也將 - 希望 - 結束當前主流標準 CommonJS 和 AMD 之間的分歧。擁有單一的、原生的模組標準意味著

16.11 進一步閱讀

下一頁:IV 儲存