## 24.模塊
> 原文: [http://exploringjs.com/impatient-js/ch_modules.html](http://exploringjs.com/impatient-js/ch_modules.html)
JavaScript 模塊的當前環境非常多樣化:ES6 帶來了內置模塊,但是它們之前的模塊系統仍然存在。了解后者有助于理解前者,所以讓我們進行調查。
### 24.1。在模塊之前:腳本
最初,瀏覽器只有 _ 腳本 _ - 在全局范圍內執行的代碼片段。例如,考慮一個 HTML 文件,它通過以下 HTML 元素加載 _ 腳本文件 _:
```html
<script src="my-library.js"></script>
```
在腳本文件中,我們模擬一個模塊:
```js
var myModule = function () { // Open IIFE
// Imports (via global variables)
var importedFunc1 = otherLibrary1.importedFunc1;
var importedFunc2 = otherLibrary2.importedFunc2;
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports (assigned to global variable `myModule`)
return {
exportedFunc: exportedFunc,
};
}(); // Close IIFE
```
在我們開始使用實際模塊(在 ES6 中引入)之前,所有代碼都是用 ES5 編寫的(沒有`const`和`let`,只有`var`)。
`myModule`是一個全局變量。定義模塊的代碼包含在 _ 立即調用的函數表達式 _(IIFE)中。創建一個函數并立即調用它,與直接執行代碼相比只有一個好處(不包裝它):在 IIFE 中定義的所有變量都保持在其范圍內,不會變為全局變量。最后,我們選擇要導出的內容并通過對象字面值返回。這種模式被稱為 _ 揭示模塊模式 _(由 Christian Heilmann 創造)。
這種模擬模塊的方法有幾個問題:
* 腳本文件中的庫通過全局變量導出和導入功能,這會冒名稱沖突的風險。
* 沒有明確聲明依賴關系,并且腳本沒有內置的方法來加載它所依賴的腳本。因此,網頁不僅要加載頁面所需的腳本,還要加載這些腳本的依賴關系,依賴項的依賴關系等等。它必須按正確的順序執行!
### 24.2。在 ES6 之前創建的模塊系統
在 ECMAScript 6 之前,JavaScript 沒有內置模塊。因此,該語言的靈活語法用于在語言中實現自定義模塊系統 _。兩個流行的是 CommonJS(針對服務器端)和 AMD(異步模塊定義,針對客戶端)。_
#### 24.2.1。服務器端:CommonJS 模塊
模塊的原始 CommonJS 標準主要是為服務器和桌面平臺創建的。它是 Node.js 模塊系統的基礎,在那里它獲得了令人難以置信的流行度。對這種受歡迎程度的貢獻是 Node 的軟件包管理器 npm,以及支持在客戶端使用 Node 模塊(browserify 和 webpack)的工具。
從現在開始,我可以互換地使用術語 _CommonJS 模塊 _ 和 _Node.js 模塊 _,即使 Node.js 還有一些額外的功能。以下是 Node.js 模塊的示例。
```js
// Imports
var importedFunc1 = require('other-module1').importedFunc1;
var importedFunc2 = require('other-module2').importedFunc2;
// Body
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
// Exports
module.exports = {
exportedFunc: exportedFunc,
};
```
CommonJS 的特征如下:
* 專為服務器設計。
* 模塊意味著同步加載。
* 緊湊的語法。
#### 24.2.2。客戶端:AMD(異步模塊定義)模塊
創建 AMD 模塊格式是為了在瀏覽器中比 CommonJS 格式更容易使用。它最受歡迎的實現是 RequireJS。以下是 RequireJS 模塊的示例。
```js
define(['other-module1', 'other-module2'],
function (otherModule1, otherModule2) {
var importedFunc1 = otherModule1.importedFunc1;
var importedFunc2 = otherModule2.importedFunc2;
function internalFunc() {
// ···
}
function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
return {
exportedFunc: exportedFunc,
};
});
```
AMD 的特點如下:
* 專為瀏覽器設計。
* 模塊意味著異步加載。這對于瀏覽器來說是一個至關重要的要求,代碼不能等到模塊下載完畢。必須在模塊可用時通知它。
* 語法稍微復雜一些。從好的方面來說,AMD 模塊可以直接執行,無需自定義創建和執行源代碼(想想`eval()`)。網上并不總是允許這樣做。
#### 24.2.3。 JavaScript 模塊的特征
看看 CommonJS 和 AMD,JavaScript 模塊系統之間的相似之處出現了:
* 每個文件有一個模塊(AND 每個文件也支持多個模塊)。
* 這樣的文件基本上是一段執行的代碼:
* 導出:該代碼包含聲明(變量,函數等)。默認情況下,這些聲明保留在模塊的本地,但您可以將其中一些聲明標記為導出。
* 導入:模塊可以從其他模塊導入實體。那些其他模塊通過 _ 模塊說明符 _(通常是路徑,偶爾 URL)來識別。
* 模塊是 _ 單例 _:即使多次導入模塊,也只存在單個實例。
* 沒有使用全局變量。相反,模塊說明符用作全局 ID。
### 24.3。 ECMAScript 模塊
ESAS 引入了 ECMAScript 模塊:它們堅定地遵循 JavaScript 模塊的傳統,并分享現有模塊系統的許多特性:
* 使用 CommonJS,ES 模塊共享緊湊語法,單個導出的語法比 _ 命名導出 _(到目前為止,我們只看到命名導出)和支持循環依賴關系更好。
* 對于 AMD,ES 模塊共享異步加載和可配置模塊加載的設計(例如,如何解析說明符)。
ES 模塊也有新的好處:
* 它們的語法比 CommonJS 更緊湊。
* 它們的模塊具有靜態結構(在運行時無法更改)。這樣可以實現靜態檢查,優化的導入訪問,更好的捆綁(交付更少的代碼)等等。
* 他們對循環進口的支持是完全透明的。
這是 ES 模塊語法的示例:
```js
import {importedFunc1} from 'other-module1';
import {importedFunc2} from 'other-module2';
function internalFunc() {
···
}
export function exportedFunc() {
importedFunc1();
importedFunc2();
internalFunc();
}
```
從現在開始,“模塊”意味著“ECMAScript 模塊”。
#### 24.3.1。 ECMAScript 模塊:三部分
ECMAScript 模塊包括三個部分:
1. 聲明模塊語法:什么是模塊?如何申報進出口?
2. 語法的語義:如何處理由導入創建的變量綁定?如何處理導出的變量綁定?
3. 用于配置模塊加載的編程加載器 API。
第 1 部分和第 2 部分與 ES6 一起介紹。第 3 部分的工作正在進行中。
### 24.4。命名出口
每個模塊可以有零個或多個命名導出。
例如,請考慮以下三個文件:
```js
lib/my-math.js
main1.js
main2.js
```
模塊`my-math.js`有兩個命名導出:`square`和`MY_CONSTANT`。
```js
let notExported = 'abc';
export function square(x) {
return x * x;
}
export const MY_CONSTANT = 123;
```
模塊`main1.js`有一個命名導入,`square`:
```js
import {square} from './lib/my-math.js';
assert.equal(square(3), 9);
```
模塊`main2.js`有一個所謂的 _ 命名空間導入 _ - `my-math.js`的所有命名導出都可以作為對象`myMath`的屬性訪問:
```js
import * as myMath from './lib/my-math.js';
assert.equal(myMath.square(3), 9);
```
 **練習:命名出口**
`exercises/modules/export_named_test.js`
### 24.5。默認導出
每個模塊最多只能有一個默認導出。這個想法是模塊 _ 是 _ 的默認導出值。模塊可以同時具有命名導出和默認導出,但通常最好堅持每個模塊一種導出樣式。
作為默認導出的示例,請考慮以下兩個文件:
```js
my-func.js
main.js
```
模塊`my-func.js`具有默認導出:
```js
export default function () {
return 'Hello!';
}
```
模塊`main.js`默認 - 導入導出的函數:
```js
import myFunc from './my-func.js';
assert.equal(myFunc(), 'Hello!');
```
注意語法差異:命名導入周圍的花括號表示我們將 _ 傳入 _ 模塊,而默認導入 _ 是 _ 模塊。
默認導出的最常見用例是包含單個函數或單個類的模塊。
#### 24.5.1。默認導出的兩種樣式
執行默認導出有兩種樣式。
首先,您可以使用`export default`標記現有聲明:
```js
export default function foo() {} // no semicolon!
export default class Bar {} // no semicolon!
```
其次,您可以直接默認導出值。在那種風格中,`export default`本身就像一個宣言。
```js
export default 'abc';
export default foo();
export default /^xyz$/;
export default 5 * 7;
export default { no: false, yes: true };
```
為什么有兩種默認導出樣式?原因是`export default`不能用于標記`const`:`const`可能定義多個值,但`export default`只需要一個值。
```js
// Not legal JavaScript!
export default const foo = 1, bar = 2, baz = 3;
```
使用此假設代碼,您不知道三個值中的哪一個是默認導出。
 **練習:默認導出**
`exercises/modules/export_default_test.js`
### 24.6。命名模塊
命名模塊文件及其導入的變量沒有既定的最佳實踐。
在本章中,我使用了以下命名方式:
* 模塊文件的名稱是破折號的,并以小寫字母開頭:
```js
./my-module.js
./some-func.js
```
* 命名空間導入的名稱是小寫的和駝峰式的:
```js
import * as myModule from './my-module.js';
```
* 默認導入的名稱是小寫的和駝峰式的:
```js
import someFunc from './some-func.js';
```
這種風格背后的理由是什么?
* npm 不允許包名中的大寫字母( [source](npm%20doesn’t%20allow%20uppercase%20letters) )。因此,我們避免使用駝峰,因此“本地”文件的名稱與 npm 包的名稱一致。
* 將基于短劃線的文件名轉換為以駝峰為基礎的 JavaScript 變量名稱有明確的規則。由于我們如何命名命名空間導入,這些規則適用于命名空間導入和默認導入。
我也喜歡下劃線模塊文件名,因為你可以直接使用這些名稱進行名稱空間導入(沒有任何翻譯):
```js
import someFunc from './some-func.js';
```
但是這種樣式對默認導入不起作用:我喜歡下劃線外殼用于命名空間對象,但它不是函數等的好選擇。
### 24.7。導入是導出的只讀視圖
到目前為止,我們直觀地使用了進口和出口,一切似乎都按預期運作。但現在是時候仔細研究進出口的真實關系了。
考慮以下兩個模塊:
```js
counter.js
main.js
```
`counter.js`導出一個(mutable!)變量和一個函數:
```js
export let counter = 3;
export function incCounter() {
counter++;
}
```
`main.js` name-導入兩個導出。當我們使用`incCounter()`時,我們發現與`counter`的連接是實時的 - 我們總是可以訪問該變量的實時狀態:
```js
import { counter, incCounter } from './counter.js';
// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);
```
請注意,雖然連接是實時的并且我們可以讀取`counter`,但我們無法更改此變量(例如,通過`counter++`)。
為什么 ES 模塊會以這種方式運行?
首先,分割模塊更容易,因為以前的共享變量可以成為導出。
其次,這種行為對于循環導入至關重要。在執行模塊之前,模塊的導出是已知的。因此,如果模塊 L 和模塊 M 相互導入,則循環地執行以下步驟:
* L 的執行開始。
* L 進口 M. L's 進口指向 M 內的未初始化槽。
* L'的尸體尚未執行。
* M 的執行開始(由導入觸發)。
* M 進口 L.
* M 的主體被執行。現在 L's 進口有值(由于實時連接)。
* L 的主體被執行。現在 M 的進口有值。
循環導入是您應該盡可能避免的,但它們可能出現在復雜系統或重構系統中。重要的是,當發生這種情況時,事情不會破裂。
### 24.8。模塊說明符
一個關鍵規則是:
> 所有 ES 模塊說明符必須是有效的 URL 并指向實際文件。
除此之外,一切仍然有點不穩定。
#### 24.8.1。模塊說明符的類別
在我們進一步了解之前,我們需要建立以下類別的模塊說明符(源自 CommonJS):
* 相對路徑:以點開頭。例子:
```js
'./some/other/module.js'
'../../lib/counter.js'
```
* 絕對路徑:以斜杠開頭。例:
```js
'/home/jane/file-tools.js'
```
* 完整的 URL:包括協議(從技術上講,路徑也是 URL)。例:
```js
'https://example.com/some-module.js'
```
* 裸路徑:不要以點,斜線或協議開頭。在 CommonJS 模塊中,裸路徑很少有文件擴展名。
```js
'lodash'
'mylib/string-tools'
'foo/dist/bar.js'
```
#### 24.8.2。 Node.js 中的 ES 模塊說明符
Node.js 中對 ES 模塊的支持正在進行中。目前的計劃(截至 2018-12-20)是按如下方式處理模塊說明符:
* 相對路徑,絕對路徑和完整 URL 按預期工作。他們都必須指向真實的文件。
* 裸路徑:
* 內置模塊(`path`,`fs`等)可以通過裸路徑導入。
* 所有其他裸路徑必須指向文件:`'foo/dist/bar.js'`
* ES 模塊的默認文件擴展名為`.mjs`(可能有一種方法可以切換到每個包的不同擴展名)。
#### 24.8.3。瀏覽器中的 ES 模塊說明符
瀏覽器處理模塊說明符如下:
* 相對路徑,絕對路徑和完整 URL 按預期工作。他們都必須指向真實的文件。
* 最終如何處理裸路徑尚不清楚。您最終可以通過查找表將它們映射到其他說明符。
* 模塊的文件擴展名無關緊要,只要它們與內容類型`text/javascript`一起提供即可。
請注意,將模塊說明符編譯為單個文件的瀏覽器和 webpack 等捆綁工具對模塊說明符的限制要少于瀏覽器,因為它們在編譯時運行,而不是在運行時運行。
### 24.9。語法缺陷:導入不是解構
導入和解構看起來都很相似:
```js
import {foo} from './bar.js'; // import
const {foo} = require('./bar.js'); // destructuring
```
但它們完全不同:
* 進口與出口保持聯系。
* 您可以在解構模式中再次進行解構,但導入語句中的`{}`不能嵌套。
* 重命名的語法不同:
```js
import {foo as f} from './bar.js'; // importing
const {foo: f} = require('./bar.js'); // destructuring
```
理由:解構讓人想起對象字面值(包括嵌套),而導入則喚起重命名的想法。
### 24.10。預覽:動態加載模塊
到目前為止,導入模塊的唯一方法是通過`import`語句。這些語句的局限性:
* 您必須在模塊的頂層使用它們。也就是說,當你在一個街區內時,你不能導入一些東西。
* 模塊說明符始終是固定的。也就是說,您無法根據條件更改導入的內容,也無法動態檢索或組裝說明符。
[即將推出的 JavaScript 功能](https://github.com/tc39/proposal-dynamic-import)改變了:`import()`運算符,它被用作異步函數(它只是一個運算符,因為它需要隱式訪問當前模塊的 URL)。
請考慮以下文件:
```js
lib/my-math.js
main1.js
main2.js
```
我們已經看過模塊`my-math.js`:
```js
let notExported = 'abc';
export function square(x) {
return x * x;
}
export const MY_CONSTANT = 123;
```
這是在`main1.js`中使用`import()`的樣子:
```js
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';
function loadConstant() {
return import(moduleSpecifier)
.then(myMath => {
const result = myMath.MY_CONSTANT;
assert.equal(result, 123);
return result;
});
}
```
方法`.then()`是 _Promises_ 的一部分,這是一種處理異步結果的機制,本書稍后將對此進行介紹。
此代碼中的兩件事在以前是不可能的:
* 我們在函數內部導入(不在頂層)。
* 模塊說明符來自變量。
接下來,我們將在`main2.js`中實現完全相同的功能,但是通過所謂的 _ 異步函數 _,它為 Promises 提供了更好的語法。
```js
const dir = './lib/';
const moduleSpecifier = dir + 'my-math.js';
async function loadConstant() {
const myMath = await import(moduleSpecifier);
const result = myMath.MY_CONSTANT;
assert.equal(result, 123);
return result;
}
```
唉,`import()`還不是 JavaScript 的標準版本,但可能會相對較快。這意味著支持是混合的,可能不一致。
### 24.11。進一步閱讀
* 更多關于`import()`:[“ES 提案:`import()` - 在 2ality 上動態導入 ES 模塊”](http://2ality.com/2017/01/import-operator.html)。
* 有關 ECMAScript 模塊的深入了解,請參考[“探索 ES6”](http://exploringjs.com/es6/ch_modules.html)。
 **測驗**
參見[測驗應用程序](ch_quizzes-exercises.html#quizzes)。
- I.背景
- 1.關于本書(ES2019 版)
- 2.常見問題:本書
- 3. JavaScript 的歷史和演變
- 4.常見問題:JavaScript
- II.第一步
- 5.概覽
- 6.語法
- 7.在控制臺上打印信息(console.*)
- 8.斷言 API
- 9.測驗和練習入門
- III.變量和值
- 10.變量和賦值
- 11.值
- 12.運算符
- IV.原始值
- 13.非值undefined和null
- 14.布爾值
- 15.數字
- 16. Math
- 17. Unicode - 簡要介紹(高級)
- 18.字符串
- 19.使用模板字面值和標記模板
- 20.符號
- V.控制流和數據流
- 21.控制流語句
- 22.異常處理
- 23.可調用值
- VI.模塊化
- 24.模塊
- 25.單個對象
- 26.原型鏈和類
- 七.集合
- 27.同步迭代
- 28.數組(Array)
- 29.類型化數組:處理二進制數據(高級)
- 30.映射(Map)
- 31. WeakMaps(WeakMap)
- 32.集(Set)
- 33. WeakSets(WeakSet)
- 34.解構
- 35.同步生成器(高級)
- 八.異步
- 36. JavaScript 中的異步編程
- 37.異步編程的 Promise
- 38.異步函數
- IX.更多標準庫
- 39.正則表達式(RegExp)
- 40.日期(Date)
- 41.創建和解析 JSON(JSON)
- 42.其余章節在哪里?