了解JavaScript中的模块和导入导出语句

来自菜鸟教程
跳转至:导航、​搜索

作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。

介绍

在 Web 的早期,网站主要由 HTMLCSS 组成。 如果将任何 JavaScript 加载到页面中,它通常以小片段的形式提供效果和交互性。 因此,JavaScript 程序通常完全编写在一个文件中并加载到 script 标记中。 开发人员可以将 JavaScript 分解为多个文件,但所有变量和 函数 仍将添加到全局 范围

但是随着网站随着 AngularReactVue 等框架的出现而不断发展,并且随着公司创建高级 Web 应用程序而不是桌面应用程序,JavaScript 现在在浏览器中扮演着重要角色。 因此,更需要使用第三方代码来执行常见任务,将代码分解为模块化文件,并避免污染全局命名空间。

ECMAScript 2015 规范将 modules 引入 JavaScript 语言,允许使用 importexport 语句。 在本教程中,您将了解什么是 JavaScript 模块以及如何使用 importexport 来组织代码。

模块化编程

在 JavaScript 中出现模块概念之前,当开发人员想要将他们的代码组织成段时,他们会创建多个文件并作为单独的脚本链接到它们。 为了演示这一点,创建一个示例 index.html 文件和两个 JavaScript 文件,functions.jsscript.js

index.html 文件将显示两个数的和、差、积、商,并链接到 script 标签中的两个 JavaScript 文件。 在文本编辑器中打开 index.html 并添加以下代码:

索引.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <title>JavaScript Modules</title>
  </head>

  <body>
    <h1>Answers</h1>
    <h2><strong id="x"></strong> and <strong id="y"></strong></h2>

    <h3>Addition</h3>
    <p id="addition"></p>

    <h3>Subtraction</h3>
    <p id="subtraction"></p>

    <h3>Multiplication</h3>
    <p id="multiplication"></p>

    <h3>Division</h3>
    <p id="division"></p>

    <script src="functions.js"></script>
    <script src="script.js"></script>
  </body>
</html>

此 HTML 将在 h2 标头中显示变量 xy 的值,并在以下 p 元素中显示对这些变量的操作值。 元素的 id 属性设置为 DOM 操作,这将发生在 script.js 文件中; 此文件还将设置 xy 的值。 有关 HTML 的更多信息,请查看我们的 如何使用 HTML 构建网站系列。

functions.js 文件将包含将在第二个脚本中使用的数学函数。 打开 functions.js 文件并添加以下内容:

函数.js

function sum(x, y) {
  return x + y
}

function difference(x, y) {
  return x - y
}

function product(x, y) {
  return x * y
}

function quotient(x, y) {
  return x / y
}

最后,script.js 文件将确定 xy 的值,对它们应用函数,并显示结果:

脚本.js

const x = 10
const y = 5

document.getElementById('x').textContent = x
document.getElementById('y').textContent = y

document.getElementById('addition').textContent = sum(x, y)
document.getElementById('subtraction').textContent = difference(x, y)
document.getElementById('multiplication').textContent = product(x, y)
document.getElementById('division').textContent = quotient(x, y)

设置这些文件并保存后,您可以 在浏览器中打开 index.html 以显示您的网站以及所有结果:

对于有少量小脚本的网站,这是一种有效的代码划分方式。 但是,这种方法存在一些问题,包括:

  • 污染全局命名空间:您在脚本中创建的所有变量——sumdifference 等——现在都存在于 window 对象中。 如果您尝试在另一个文件中使用另一个名为 sum 的变量,则很难知道脚本中的任何时候将使用哪个值,因为它们都将使用相同的 window.sum多变的。 变量私有的唯一方法是将其放在函数范围内。 DOM 中名为 xvar xid 甚至可能存在冲突。
  • 依赖管理:脚本必须按从上到下的顺序加载,以确保正确的变量可用。 将脚本保存为不同的文件会产生分离的错觉,但它本质上与在浏览器页面中具有单个内联 <script> 相同。

在 ES6 将原生模块添加到 JavaScript 语言之前,社区试图提出几种解决方案。 第一个解决方案是用原生 JavaScript 编写的,例如在 对象立即调用函数表达式 (IIFE) 中编写所有代码,并将它们放在全局命名空间中的单个对象上。 这是对多脚本方法的改进,但仍然存在将至少一个对象放在全局命名空间中的相同问题,并且没有使第三方之间持续共享代码的问题变得更容易。

之后,出现了一些模块解决方案:CommonJS,一种在Node.js中实现的同步方法,异步模块定义(AMD),这是一个异步方法和 通用模块定义 (UMD),它旨在成为一种支持以前两种样式的通用方法。

这些解决方案的出现使开发人员更容易以 的形式共享和重用代码,这些模块可以分发和共享,例如 npm 上的模块。 然而,由于有许多解决方案并且没有一个是 JavaScript 原生的,因此必须实现诸如 BabelWebpackBrowserify 之类的工具才能在浏览器中使用模块。

由于多文件方法的许多问题以及所提出的解决方案的复杂性,开发人员有兴趣将 模块化编程 方法引入 JavaScript 语言。 因此,ECMAScript 2015 支持使用 JavaScript 模块。

module 是一组代码,它充当接口,为其他模块提供功能,并能够依赖其他模块的功能。 模块 导出 以提供代码, 导入 以使用其他代码。 模块很有用,因为它们允许开发人员重用代码,它们提供了许多开发人员可以使用的稳定、一致的接口,并且它们不会污染全局命名空间。

模块(有时称为 ECMAScript 模块或 ES 模块)现在可以在 JavaScript 中本地使用,在本教程的其余部分中,您将探索如何在代码中使用和实现它们。

原生 JavaScript 模块

JavaScript 中的模块使用 importexport 关键字:

  • import:用于读取从另一个模块导出的代码。
  • export:用于向其他模块提供代码。

为了演示如何使用它,请将您的 functions.js 文件更新为模块并导出函数。 您将在每个函数前面添加 export,这将使它们可用于任何其他模块。

将以下突出显示的代码添加到您的文件中:

函数.js

export function sum(x, y) {
  return x + y
}

export function difference(x, y) {
  return x - y
}

export function product(x, y) {
  return x * y
}

export function quotient(x, y) {
  return x / y
}

现在,在 script.js 中,您将使用 import 从文件顶部的 functions.js 模块中检索代码。

注意import必须始终位于文件顶部,在任何其他代码之前,并且还必须包含相对路径(本例中为./)。


将以下突出显示的代码添加到 script.js

脚本.js

import { sum, difference, product, quotient } from './functions.js'

const x = 10
const y = 5

document.getElementById('x').textContent = x
document.getElementById('y').textContent = y

document.getElementById('addition').textContent = sum(x, y)
document.getElementById('subtraction').textContent = difference(x, y)
document.getElementById('multiplication').textContent = product(x, y)
document.getElementById('division').textContent = quotient(x, y)

请注意,单个函数是通过在花括号中命名它们来导入的。

为了确保此代码作为模块而不是常规脚本加载,请将 type="module" 添加到 index.html 中的 script 标记。 任何使用 importexport 的代码都必须使用此属性:

索引.html

...
<script type="module" src="functions.js"></script>
<script type="module" src="script.js"></script>

此时,您将能够重新加载包含更新的页面,并且网站现在将使用模块。 浏览器支持非常高,但是可以使用 caniuse 来检查哪些浏览器支持它。 请注意,如果您将文件视为指向本地文件的直接链接,则会遇到此错误:

OutputAccess to script at 'file:///Users/your_file_path/script.js' from origin 'null' has been blocked by CORS policy: Cross-origin requests are only supported for protocol schemes: http, data, chrome, chrome-extension, chrome-untrusted, https.

由于 CORS 政策,模块必须在服务器环境中使用,您可以使用 http-server 在本地或通过托管服务提供商在 Internet 上进行设置。

模块在以下几个方面不同于常规脚本:

  • 模块不会向全局 (window) 范围添加任何内容。
  • 模块始终处于 严格模式
  • 在同一个文件中加载同一个模块两次没有效果,因为模块只执行一次。
  • 模块需要服务器环境。

模块仍然经常与 Webpack 等捆绑器一起使用,以增加对浏览器的支持和附加功能,但它们也可以直接在浏览器中使用。

接下来,您将探索使用 importexport 语法的更多方式。

命名导出

如前所述,使用 export 语法将允许您单独导入已按名称导出的值。 例如,以 functions.js 的这个简化版本为例:

函数.js

export function sum() {}
export function difference() {}

这将允许您使用花括号按名称导入 sumdifference

脚本.js

import { sum, difference } from './functions.js'

也可以使用别名来重命名函数。 您可以这样做以避免同一模块内的命名冲突。 在本例中,sum 将重命名为 adddifference 将重命名为 subtract

脚本.js

import {
  sum as add,
  difference as subtract
} from './functions.js'

add(1, 2) // 3

在此处调用 add() 将产生 sum() 函数的结果。

使用 * 语法,您可以将整个模块的内容导入到一个对象中。 在这种情况下,sumdifference 将成为 mathFunctions 对象上的方法。

脚本.js

import * as mathFunctions from './functions.js'

mathFunctions.sum(1, 2) // 3
mathFunctions.difference(10, 3) // 7

原始值、函数表达式和定义、异步函数和实例化类都可以导出,只要它们有标识符:

// Primitive values
export const number = 100
export const string = 'string'
export const undef = undefined
export const empty = null
export const obj = { name: 'Homer' }
export const array = ['Bart', 'Lisa', 'Maggie']

// Function expression
export const sum = (x, y) => x + y

// Function definition
export function difference(x, y) {
  return x - y
}

// Asynchronous function
export async function getBooks() {}

// Class
export class Book {
  constructor(name, author) {
    this.name = name
    this.author = author
  }
}

// Instantiated class
export const book = new Book('Lord of the Rings', 'J. R. R. Tolkien')

所有这些导出都可以成功导入。 您将在下一节中探讨的另一种导出类型称为默认导出。

默认导出

在前面的示例中,您导出了多个命名导出并将它们单独导入或作为一个对象导入,每个导出作为对象上的一个方法。 模块还可以包含默认导出,使用 default 关键字。 默认导出不会使用大括号导入,而是直接导入到命名标识符中。

例如,functions.js文件的内容如下:

函数.js

export default function sum(x, y) {
  return x + y
}

script.js 文件中,您可以将默认函数导入为 sum,如下所示:

脚本.js

import sum from './functions.js'

sum(1, 2) // 3

这可能很危险,因为在导入期间您可以命名默认导出没有任何限制。 在这个例子中,默认函数被导入为 difference 虽然它实际上是 sum 函数:

脚本.js

import difference from './functions.js'

difference(1, 2) // 3

因此,通常首选使用命名导出。 与命名导出不同,默认导出不需要标识符——原始值本身或匿名函数都可以用作默认导出。 以下是用作默认导出的对象示例:

函数.js

export default {
  name: 'Lord of the Rings',
  author: 'J. R. R. Tolkien',
}

您可以使用以下命令将其导入为 book

脚本.js

import book from './functions.js'

同样,以下示例演示了将匿名 箭头函数 导出为默认导出:

函数.js

export default () => 'This function is anonymous'

这可以使用以下 script.js 导入:

脚本.js

import anonymousFunction from './functions.js'

命名导出和默认导出可以一起使用,如在此模块中导出两个命名值和一个默认值:

函数.js

export const length = 10
export const width = 5

export default function perimeter(x, y) {
  return 2 * (x + y)
}

您可以使用以下命令导入这些变量和默认函数:

脚本.js

import calculatePerimeter, { length, width } from './functions.js'

calculatePerimeter(length, width) // 30

现在脚本可以使用默认值和命名值。

结论

模块化编程设计实践允许您将代码分成单独的组件,这有助于使您的代码可重用和一致,同时还可以保护全局命名空间。 可以使用 importexport 关键字在原生 JavaScript 中实现模块接口。

在本文中,您了解了 JavaScript 中模块的历史,如何将 JavaScript 文件分成多个顶级脚本,如何使用模块化方法更新这些文件,以及 import 和 [X217X ] 命名和默认导出的语法。

要了解有关 JavaScript 模块的更多信息,请阅读 Mozilla 开发人员网络上的 Modules。 如果您想探索 Node.js 中的模块,请尝试我们的 如何创建 Node.js 模块教程