面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

27 模块



27.1 备忘单:模块

27.1.1 导出

// Named exports
export function f() {}
export const one = 1;
export {foo, b as bar};

// Default exports
export default function f() {} // declaration with optional name
// Replacement for `const` (there must be exactly one value)
export default 123;

// Re-exporting from another module
export {foo, b as bar} from './some-module.mjs';
export * from './some-module.mjs';
export * as ns from './some-module.mjs'; // ES2020

27.1.2 导入

// Named imports
import {foo, bar as b} from './some-module.mjs';
// Namespace import
import * as someModule from './some-module.mjs';
// Default import
import someModule from './some-module.mjs';

// Combinations:
import someModule, * as someModule from './some-module.mjs';
import someModule, {foo, bar as b} from './some-module.mjs';

// Empty import (for modules with side effects)
import './some-module.mjs';

27.2 JavaScript 源代码格式

当前 JavaScript 模块的格局相当多样化:ES6 带来了内置模块,但在此之前出现的源代码格式仍然存在。了解后者有助于理解前者,所以让我们来研究一下。接下来的部分将描述以下几种交付 JavaScript 源代码的方式

表 18 概述了这些代码格式。请注意,对于 CommonJS 模块和 ECMAScript 模块,通常使用两种文件名扩展名。哪一种合适取决于我们想如何使用文件。本章稍后将详细介绍。

表 18:交付 JavaScript 源代码的方式。
运行于 加载方式 文件名扩展名
脚本 浏览器 异步 .js
CommonJS 模块 服务器 同步 .js .cjs
AMD 模块 浏览器 异步 .js
ECMAScript 模块 浏览器和服务器 异步 .js .mjs

27.2.1 内置模块之前的代码是用 ECMAScript 5 编写的

在我们接触到内置模块(ES6 引入的)之前,我们将看到的所有代码都将用 ES5 编写。除此之外

27.3 在我们有模块之前,我们有脚本

最初,浏览器只有脚本——在全局作用域中执行的代码片段。例如,考虑一个通过以下 HTML 加载脚本文件的 HTML 文件

<script src="other-module1.js"></script>
<script src="other-module2.js"></script>
<script src="my-module.js"></script>

主文件是 my-module.js,我们在其中模拟一个模块

var myModule = (function () { // Open IIFE
  // Imports (via global variables)
  var importedFunc1 = otherModule1.importedFunc1;
  var importedFunc2 = otherModule2.importedFunc2;

  // Body
  function internalFunc() {
    // ···
  }
  function exportedFunc() {
    importedFunc1();
    importedFunc2();
    internalFunc();
  }

  // Exports (assigned to global variable `myModule`)
  return {
    exportedFunc: exportedFunc,
  };
})(); // Close IIFE

myModule 是一个全局变量,它被赋值为立即调用函数表达式的结果。函数表达式从第一行开始。它在最后一行被调用。

这种包装代码片段的方式称为立即调用函数表达式(IIFE,由 Ben Alman 提出)。我们从 IIFE 中获得了什么?var 不是块级作用域的(如 constlet),它是函数级作用域的:为 var 声明的变量创建新作用域的唯一方法是通过函数或方法(使用 constlet,我们可以使用函数、方法或块 {})。因此,示例中的 IIFE 隐藏了以下所有变量,使其不在全局作用域中,并最大限度地减少了名称冲突:importedFunc1importedFunc2internalFuncexportedFunc

请注意,我们以一种特殊的方式使用 IIFE:最后,我们选择要导出的内容,并通过对象字面量返回它。这被称为揭示模块模式(由 Christian Heilmann 提出)。

这种模拟模块的方式有几个问题

27.4 ES6 之前创建的模块系统

在 ECMAScript 6 之前,JavaScript 没有内置模块。因此,该语言灵活的语法被用来在语言内部实现自定义模块系统。两种流行的模块系统是

27.4.1 服务器端:CommonJS 模块

最初的 CommonJS 模块标准是为服务器和桌面平台创建的。它是最初的 Node.js 模块系统的基础,并在那里获得了巨大的流行。Node 的 npm 包管理器以及能够在客户端使用 Node 模块的工具(browserify、webpack 等)也促成了它的流行。

从现在开始,CommonJS 模块指的是该标准的 Node.js 版本(它有一些额外的功能)。这是一个 CommonJS 模块的例子

// Imports
var importedFunc1 = require('./other-module1.js').importedFunc1;
var importedFunc2 = require('./other-module2.js').importedFunc2;

// Body
function internalFunc() {
  // ···
}
function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

// Exports
module.exports = {
  exportedFunc: exportedFunc,
};

CommonJS 的特点如下

27.4.2 客户端:AMD(异步模块定义)模块

创建 AMD 模块格式是为了比 CommonJS 格式更易于在浏览器中使用。它最流行的实现是 RequireJS。下面是一个 AMD 模块的例子。

define(['./other-module1.js', './other-module2.js'],
  function (otherModule1, otherModule2) {
    var importedFunc1 = otherModule1.importedFunc1;
    var importedFunc2 = otherModule2.importedFunc2;

    function internalFunc() {
      // ···
    }
    function exportedFunc() {
      importedFunc1();
      importedFunc2();
      internalFunc();
    }
    
    return {
      exportedFunc: exportedFunc,
    };
  });

AMD 的特点如下

从好的方面来说,AMD 模块可以直接执行。相比之下,CommonJS 模块必须在部署之前进行编译,或者必须动态生成和评估自定义源代码 (想想 eval())。这在 Web 上并不总是允许的。

27.4.3 JavaScript 模块的特点

纵观 CommonJS 和 AMD,JavaScript 模块系统之间的相似之处就显现出来了

27.5 ECMAScript 模块

ECMAScript 模块ES 模块ESM)是 ES6 引入的。它们延续了 JavaScript 模块的传统,并具有上述所有特点。此外

ES 模块还有新的优势

这是一个 ES 模块语法的例子

import {importedFunc1} from './other-module1.mjs';
import {importedFunc2} from './other-module2.mjs';

function internalFunc() {
  ···
}

export function exportedFunc() {
  importedFunc1();
  importedFunc2();
  internalFunc();
}

从现在开始,“模块”指的是“ECMAScript 模块”。

27.5.1 ES 模块:语法、语义、加载器 API

ES 模块的完整标准包括以下部分

  1. 语法(代码如何编写):什么是模块?如何声明导入和导出?等等。
  2. 语义(代码如何执行):如何导出变量绑定?如何将导入与导出连接起来?等等。
  3. 用于配置模块加载的编程加载器 API。

第 1 部分和第 2 部分是 ES6 引入的。第 3 部分的工作正在进行中。

27.6 命名导出和导入

27.6.1 命名导出

每个模块可以有零个或多个命名导出

例如,考虑以下两个文件

lib/my-math.mjs
main.mjs

模块 my-math.mjs 有两个命名导出:squareLIGHTSPEED

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

要导出某些内容,我们在声明前面加上关键字 export。未导出的实体对模块是私有的,不能从外部访问。

27.6.2 命名导入

模块 main.mjs 有一个命名导入,square

import {square} from './lib/my-math.mjs';
assert.equal(square(3), 9);

它还可以重命名其导入

import {square as sq} from './lib/my-math.mjs';
assert.equal(sq(3), 9);
27.6.2.1 语法陷阱:命名导入不是解构

命名导入和解构看起来很相似

import {foo} from './bar.mjs'; // import
const {foo} = require('./bar.mjs'); // destructuring

但它们有很大不同

  练习:命名导出

exercises/modules/export_named_test.mjs

27.6.3 命名空间导入

命名空间导入是命名导入的替代方案。如果我们命名空间导入一个模块,它将成为一个对象,其属性是命名导出。如果我们使用命名空间导入,则 main.mjs 如下所示

import * as myMath from './lib/my-math.mjs';
assert.equal(myMath.square(3), 9);

assert.deepEqual(
  Object.keys(myMath), ['LIGHTSPEED', 'square']);

27.6.4 命名导出样式:内联与子句(高级)

到目前为止,我们看到的命名导出样式是内联的:我们通过在实体前面加上关键字 export 来导出它们。

但我们也可以使用单独的导出子句。例如,以下是使用导出子句的 lib/my-math.mjs

function times(a, b) {
  return a * b;
}
function square(x) {
  return times(x, x);
}
const LIGHTSPEED = 299792458;

export { square, LIGHTSPEED }; // semicolon!

使用导出子句,我们可以在导出之前重命名并在内部使用不同的名称

function times(a, b) {
  return a * b;
}
function sq(x) {
  return times(x, x);
}
const LS = 299792458;

export {
  sq as square,
  LS as LIGHTSPEED, // trailing comma is optional
};

27.7 默认导出和导入

每个模块最多可以有一个默认导出。其理念是模块就是默认导出的值。

  避免混合使用命名导出和默认导出

一个模块可以同时具有命名导出和默认导出,但通常最好每个模块坚持一种导出样式。

作为默认导出的示例,请考虑以下两个文件

my-func.mjs
main.mjs

模块 my-func.mjs 具有默认导出

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

模块 main.mjs 默认导入导出的函数

import myFunc from './my-func.mjs';
assert.equal(myFunc(), 'Hello!');

请注意语法差异:命名导入周围的花括号表示我们正在深入模块,而默认导入就是模块。

  默认导出的用例是什么?

默认导出最常见的用例是包含单个函数或单个类的模块。

27.7.1 默认导出的两种样式

默认导出有两种样式。

首先,我们可以使用 export default 标记现有声明

export default function myFunc() {} // no semicolon!
export default class MyClass {} // no semicolon!

其次,我们可以直接默认导出值。这种 export default 样式很像声明。

export default myFunc; // defined elsewhere
export default MyClass; // defined previously
export default Math.sqrt(2); // result of invocation is default-exported
export default 'abc' + 'def';
export default { no: false, yes: true };
27.7.1.1 为什么会有两种默认导出样式?

原因是 export default 不能用于标记 constconst 可以定义多个值,但 export default 只需要一个值。请考虑以下假设代码

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

使用此代码,我们不知道三个值中的哪一个是默认导出。

  练习:默认导出

exercises/modules/export_default_test.mjs

27.7.2 默认导出作为命名导出(高级)

在内部,默认导出只是一个名为 default 的命名导出。例如,请考虑前面带有默认导出的模块 my-func.mjs

const GREETING = 'Hello!';
export default function () {
  return GREETING;
}

以下模块 my-func2.mjs 等效于该模块

const GREETING = 'Hello!';
function greet() {
  return GREETING;
}

export {
  greet as default,
};

对于导入,我们可以使用普通的默认导入

import myFunc from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

或者我们可以使用命名导入

import {default as myFunc} from './my-func2.mjs';
assert.equal(myFunc(), 'Hello!');

默认导出也可以通过命名空间导入的属性 .default 获得

import * as mf from './my-func2.mjs';
assert.equal(mf.default(), 'Hello!');

  default 作为变量名不是非法的吗?

default 不能是变量名,但它可以是导出名,也可以是属性名

const obj = {
  default: 123,
};
assert.equal(obj.default, 123);

27.8 有关导出和导入的更多详细信息

27.8.1 导入是对导出的只读视图

到目前为止,我们已经直观地使用了导入和导出,并且一切似乎都按预期工作。但现在是时候仔细看看导入和导出是如何真正相关的了。

请考虑以下两个模块

counter.mjs
main.mjs

counter.mjs 导出一个(可变的!)变量和一个函数

export let counter = 3;
export function incCounter() {
  counter++;
}

main.mjs 命名导入这两个导出。当我们使用 incCounter() 时,我们发现与 counter 的连接是实时的——我们始终可以访问该变量的实时状态

import { counter, incCounter } from './counter.mjs';

// The imported value `counter` is live
assert.equal(counter, 3);
incCounter();
assert.equal(counter, 4);

请注意,虽然连接是实时的,我们可以读取 counter,但我们不能更改此变量(例如,通过 counter++)。

以这种方式处理导入有两个好处

27.8.2 ESM 对循环导入的透明支持(高级)

ESM 透明地支持循环导入。要了解这是如何实现的,请考虑以下示例:图 7 显示了导入其他模块的模块的有向图。P 导入 M 是这种情况下的循环。

Figure 7: A directed graph of modules importing modules: M imports N and O, N imports P and Q, etc.

解析后,这些模块分两个阶段设置

由于 ES 模块的两个特性,这种方法可以正确处理循环导入

27.9 npm 包

npm 软件注册表是分发用于 Node.js 和 Web 浏览器的 JavaScript 库和应用程序的主要方式。它通过npm 包管理器(简称:npm)进行管理。软件以所谓的的形式分发。包是一个目录,其中包含任意文件和一个位于顶层的描述包的文件 package.json。例如,当 npm 在目录 my-package/ 中创建一个空包时,我们会得到这个 package.json

{
  "name": "my-package",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

其中一些属性包含简单的元数据

其他属性启用高级配置

有关 package.json 的更多信息,请参阅npm 文档

27.9.1 包安装在目录 node_modules/

npm 始终将包安装在目录 node_modules 中。通常会有很多这样的目录。npm 使用哪一个取决于当前所在的目录。例如,如果我们在目录 /tmp/a/b/ 中,npm 会尝试在当前目录、其父目录、父目录的父目录等中查找 node_modules。换句话说,它会搜索以下位置

安装包 some-pkg 时,npm 使用最近的 node_modules。例如,如果我们在 /tmp/a/b/ 中,并且该目录中有一个 node_modules,则 npm 会将包放在该目录中

/tmp/a/b/node_modules/some-pkg/

导入模块时,我们可以使用特殊的模块说明符来告诉 Node.js 我们要从已安装的包中导入它。稍后将解释其工作原理。现在,请考虑以下示例

// /home/jane/proj/main.mjs
import * as theModule from 'the-package/the-module.mjs';

为了找到 the-module.mjs(Node.js 更喜欢文件名扩展名 .mjs 用于 ES 模块),Node.js 会向上遍历 node_module 链并搜索以下位置

27.9.2 为什么可以使用 npm 安装前端库?

仅在 Node.js 上支持在 node_modules 目录中查找已安装的模块。那么为什么我们也可以使用 npm 为浏览器安装库呢?

这是通过打包工具(例如 webpack)实现的,这些工具在代码部署到网上之前对其进行编译和优化。在此编译过程中,npm 包中的代码经过调整,以便在浏览器中工作。

27.10 命名模块

对于命名模块文件和导入它们的变量,没有既定的最佳实践。

在本章中,我使用以下命名样式

这种样式背后的理由是什么?

我也喜欢使用下划线连接的模块文件名,因为我们可以直接将这些名称用于命名空间导入(无需任何转换)

import * as my_module from './my_module.mjs';

但这种样式不适用于默认导入:我喜欢对命名空间对象使用下划线连接,但它不适用于函数等。

27.11 模块说明符

模块说明符是标识模块的字符串。它们在浏览器和 Node.js 中的工作方式略有不同。在我们查看差异之前,我们需要了解不同类别的模块说明符。

27.11.1 模块说明符的类别

在 ES 模块中,我们区分以下类别的说明符。这些类别起源于 CommonJS 模块。

27.11.2 浏览器中的 ES 模块说明符

浏览器按如下方式处理模块说明符

请注意,打包工具(例如 webpack,它将模块组合成更少的文件)通常对说明符的严格程度低于浏览器。这是因为它们在构建/编译时(而不是运行时)运行,并且可以通过遍历文件系统来搜索文件。

27.11.3 Node.js 上的 ES 模块说明符

Node.js 按如下方式处理模块说明符

除裸路径外,所有说明符都必须引用实际文件。也就是说,ESM 不支持以下 CommonJS 功能

所有内置 Node.js 模块都可以通过裸路径获得,并具有命名的 ESM 导出 - 例如

import * as assert from 'assert/strict';
import * as path from 'path';

assert.equal(
  path.join('a/b/c', '../d'), 'a/b/d');
27.11.3.1 Node.js 上的文件名扩展名

Node.js 支持以下默认文件名扩展名

文件名扩展名 .js 代表 ESM 或 CommonJS。它是哪一种由“最近的” package.json(在当前目录、父目录等中)配置。以这种方式使用 package.json 与包无关。

在该 package.json 中,有一个属性 "type",它有两个设置

27.11.3.2 将非文件源代码解释为 CommonJS 或 ESM

并非所有由 Node.js 执行的源代码都来自文件。我们还可以通过 stdin、--eval--print 向其发送代码。命令行选项 --input-type 允许我们指定如何解释此类代码

27.12 import.meta - 当前模块的元数据 [ES2020]

对象 import.meta 保存当前模块的元数据。

27.12.1 import.meta.url

import.meta 最重要的属性是 .url,它包含一个字符串,其中包含当前模块文件的 URL - 例如

'https://example.com/code/main.mjs'

27.12.2 import.meta.url 和类 URL

URL 可通过浏览器和 Node.js 中的全局变量获得。我们可以在 Node.js 文档 中查找其完整功能。在使用 import.meta.url 时,它的构造函数特别有用

new URL(input: string, base?: string|URL)

参数 input 包含要解析的 URL。如果提供了第二个参数 base,则它可以是相对的。

换句话说,此构造函数允许我们解析相对于基本 URL 的相对路径

> new URL('other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/code/other.mjs'
> new URL('../other.mjs', 'https://example.com/code/main.mjs').href
'https://example.com/other.mjs'

这就是我们如何获得指向位于当前模块旁边的文件 data.txtURL 实例

const urlOfData = new URL('data.txt', import.meta.url);

27.12.3 Node.js 上的 import.meta.url

在 Node.js 上,import.meta.url 始终是一个带有 file: URL 的字符串 - 例如

'file:///Users/rauschma/my-module.mjs'
27.12.3.1 示例:读取模块的同级文件

许多 Node.js 文件系统操作接受带有路径的字符串或 URL 的实例。这使我们能够读取当前模块的同级文件 data.txt

import * as fs from 'fs';
function readData() {
  // data.txt sits next to current module
  const urlOfData = new URL('data.txt', import.meta.url);
  return fs.readFileSync(urlOfData, {encoding: 'UTF-8'});
}
27.12.3.2 模块 fs 和 URL

对于模块 fs 的大多数函数,我们可以通过以下方式引用文件

有关此主题的更多信息,请参阅 Node.js API 文档

27.12.3.3 在 file: URL 和路径之间转换

Node.js 模块 url 有两个函数用于在 file: URL 和路径之间转换

如果我们需要一个可以在本地文件系统中使用的路径,那么 URL 实例的属性 .pathname 并不总是有效

assert.equal(
  new URL('file:///tmp/with%20space.txt').pathname,
  '/tmp/with%20space.txt');

因此,最好使用 fileURLToPath()

import * as url from 'url';
assert.equal(
  url.fileURLToPath('file:///tmp/with%20space.txt'),
  '/tmp/with space.txt'); // result on Unix

类似地,pathToFileURL() 不仅仅是在绝对路径前面加上 'file://'

27.13 通过 import() 动态加载模块 [ES2020](高级)

  import() 运算符使用 Promise

Promise 是一种处理异步计算结果(即不是立即计算)的技术。它们在 §40 “用于异步编程的 Promise [ES6]” 中进行了说明。在您了解它们之前,推迟阅读本节可能是有意义的。

27.13.1 静态 import 语句的局限性

到目前为止,导入模块的唯一方法是通过 import 语句。该语句有几个限制

27.13.2 通过 import() 运算符进行动态导入

import() 运算符没有 import 语句的限制。它看起来像这样

import(moduleSpecifierStr)
.then((namespaceObject) => {
  console.log(namespaceObject.namedExport);
});

此运算符的使用方式类似于函数,接收一个带有模块说明符的字符串,并返回一个 Promise,该 Promise 解析为一个命名空间对象。该对象的属性是导入模块的导出。

通过 await 使用 import() 甚至更方便

const namespaceObject = await import(moduleSpecifierStr);
console.log(namespaceObject.namedExport);

请注意,await 可以在模块的顶层使用(请参阅 下一节)。

让我们看一个使用 import() 的示例。

27.13.2.1 示例:动态加载模块

考虑以下文件

lib/my-math.mjs
main1.mjs
main2.mjs

我们已经见过模块 my-math.mjs

// Not exported, private to module
function times(a, b) {
  return a * b;
}
export function square(x) {
  return times(x, x);
}
export const LIGHTSPEED = 299792458;

我们可以使用 import() 按需加载此模块

// main1.mjs
const moduleSpecifier = './lib/my-math.mjs';

function mathOnDemand() {
  return import(moduleSpecifier)
  .then(myMath => {
    const result = myMath.LIGHTSPEED;
    assert.equal(result, 299792458);
    return result;
  });
}

mathOnDemand()
.then((result) => {
  assert.equal(result, 299792458);
});

此代码中的两件事是 import 语句无法完成的

接下来,我们将实现与 main1.mjs 中相同的功能,但通过一个称为 异步函数async/await 的功能,它为 Promise 提供了更简洁的语法。

// main2.mjs
const moduleSpecifier = './lib/my-math.mjs';

async function mathOnDemand() {
  const myMath = await import(moduleSpecifier);
  const result = myMath.LIGHTSPEED;
  assert.equal(result, 299792458);
  return result;
}

  为什么 import() 是运算符而不是函数?

import() 看起来像一个函数,但不能作为函数实现

27.13.3 import() 的用例

27.13.3.1 按需加载代码

Web 应用程序的某些功能在启动时不必存在,可以按需加载。然后 import() 就派上用场了,因为我们可以将此类功能放入模块中 - 例如

button.addEventListener('click', event => {
  import('./dialogBox.mjs')
    .then(dialogBox => {
      dialogBox.open();
    })
    .catch(error => {
      /* Error handling */
    })
});
27.13.3.2 模块的条件加载

我们可能希望根据条件是否为真来加载模块。例如,一个带有 polyfill 的模块,它可以在旧平台上使用新功能

if (isLegacyPlatform()) {
  import('./my-polyfill.mjs')
    .then(···);
}
27.13.3.3 计算模块说明符

对于国际化等应用程序,如果我们可以动态计算模块说明符,则会有所帮助

import(`messages_${getLocale()}.mjs`)
  .then(···);

27.14 模块中的顶层 await [ES2022](高级)

  await 是异步函数的功能

await§41 “异步函数” 中进行了说明。在您了解异步函数之前,推迟阅读本节可能是有意义的。

我们可以在模块的顶层使用 await 运算符。如果我们这样做,模块将变为异步并以不同的方式工作。值得庆幸的是,作为程序员,我们通常不会看到这一点,因为它是由语言透明地处理的。

27.14.1 顶层 await 的用例

为什么我们想在模块的顶层使用 await 运算符?它允许我们使用异步加载的数据初始化模块。接下来的三个小节展示了它在哪些方面有用的三个示例。

27.14.1.1 动态加载模块
const params = new URLSearchParams(location.search);
const language = params.get('lang');
const messages = await import(`./messages-${language}.mjs`); // (A)

console.log(messages.welcome);

在 A 行,我们 动态导入 一个模块。多亏了顶层 await,这几乎与使用普通的静态导入一样方便。

27.14.1.2 如果模块加载失败,则使用回退
let lodash;
try {
  lodash = await import('https://primary.example.com/lodash');
} catch {
  lodash = await import('https://secondary.example.com/lodash');
}
27.14.1.3 使用加载速度最快的资源
const resource = await Promise.any([
  fetch('http://example.com/first.txt')
    .then(response => response.text()),
  fetch('http://example.com/second.txt')
    .then(response => response.text()),
]);

由于 Promise.any(),变量 resource 通过首先完成的任何下载进行初始化。

27.14.2 顶层 await 在幕后是如何工作的?

考虑以下两个文件。

first.mjs:

const response = await fetch('http://example.com/first.txt');
export const first = await response.text();

main.mjs:

import {first} from './first.mjs';
import {second} from './second.mjs';
assert.equal(first, 'First!');
assert.equal(second, 'Second!');

两者大致等效于以下代码

first.mjs:

export let first;
export const promise = (async () => { // (A)
  const response = await fetch('http://example.com/first.txt');
  first = await response.text();
})();

main.mjs:

import {promise as firstPromise, first} from './first.mjs';
import {promise as secondPromise, second} from './second.mjs';
export const promise = (async () => { // (B)
  await Promise.all([firstPromise, secondPromise]); // (C)
  assert.equal(first, 'First content!');
  assert.equal(second, 'Second content!');
})();

模块在以下情况下变为异步

  1. 它直接使用顶层 await (first.mjs)。
  2. 它导入一个或多个异步模块 (main.mjs)。

每个异步模块都导出一个 Promise(A 行和 B 行),该 Promise 在其主体执行后完成。此时,可以安全地访问该模块的导出。

在情况 (2) 中,导入模块会等待所有导入的异步模块的 Promise 都完成,然后再进入其主体(C 行)。同步模块的处理方式与往常一样。

等待的拒绝和同步异常的管理方式与异步函数相同。

27.14.3 顶层 await 的优缺点

顶层 await 的两个最重要的优点是

不利的一面是,顶层 await 会延迟导入模块的初始化。因此,最好谨慎使用它。需要更长时间的异步任务最好稍后按需执行。

但是,即使没有顶层 await 的模块也可以阻塞导入程序(例如,通过顶层的无限循环),因此阻塞本身并不是反对它的理由。

27.15 Polyfill:模拟原生 Web 平台功能(高级)

  后端也有 Polyfill

本节是关于前端开发和 Web 浏览器的,但类似的想法也适用于后端开发。

Polyfill 帮助我们解决在 JavaScript 中开发 Web 应用程序时面临的冲突

给定一个 Web 平台功能 X

每次我们的 Web 应用程序启动时,它都必须首先为可能并非随处可用的功能执行所有 polyfill。之后,我们可以确保这些功能本身可用。

27.15.1 本节资料来源

  测验

参见测验应用程序