作者选择了 COVID-19 Relief Fund 作为 Write for DOnations 计划的一部分来接受捐赠。
介绍
在 Web 的早期,网站主要由 HTML 和 CSS 组成。 如果将任何 JavaScript 加载到页面中,它通常以小片段的形式提供效果和交互性。 因此,JavaScript 程序通常完全编写在一个文件中并加载到 script
标记中。 开发人员可以将 JavaScript 分解为多个文件,但所有变量和 函数 仍将添加到全局 范围 。
但是随着网站随着 Angular、React 和 Vue 等框架的出现而不断发展,并且随着公司创建高级 Web 应用程序而不是桌面应用程序,JavaScript 现在在浏览器中扮演着重要角色。 因此,更需要使用第三方代码来执行常见任务,将代码分解为模块化文件,并避免污染全局命名空间。
ECMAScript 2015 规范将 modules 引入 JavaScript 语言,允许使用 import
和 export
语句。 在本教程中,您将了解什么是 JavaScript 模块以及如何使用 import
和 export
来组织代码。
模块化编程
在 JavaScript 中出现模块概念之前,当开发人员想要将他们的代码组织成段时,他们会创建多个文件并作为单独的脚本链接到它们。 为了演示这一点,创建一个示例 index.html
文件和两个 JavaScript 文件,functions.js
和 script.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
标头中显示变量 x
和 y
的值,并在以下 p
元素中显示对这些变量的操作值。 元素的 id
属性设置为 DOM 操作,这将发生在 script.js
文件中; 此文件还将设置 x
和 y
的值。 有关 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
文件将确定 x
和 y
的值,对它们应用函数,并显示结果:
脚本.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 以显示您的网站以及所有结果:
对于有少量小脚本的网站,这是一种有效的代码划分方式。 但是,这种方法存在一些问题,包括:
- 污染全局命名空间:您在脚本中创建的所有变量——
sum
、difference
等——现在都存在于 window 对象中。 如果您尝试在另一个文件中使用另一个名为sum
的变量,则很难知道脚本中的任何时候将使用哪个值,因为它们都将使用相同的window.sum
多变的。 变量私有的唯一方法是将其放在函数范围内。 DOM 中名为x
和var x
的id
甚至可能存在冲突。 - 依赖管理:脚本必须按从上到下的顺序加载,以确保正确的变量可用。 将脚本保存为不同的文件会产生分离的错觉,但它本质上与在浏览器页面中具有单个内联
<script>
相同。
在 ES6 将原生模块添加到 JavaScript 语言之前,社区试图提出几种解决方案。 第一个解决方案是用原生 JavaScript 编写的,例如在 对象 或 立即调用函数表达式 (IIFE) 中编写所有代码,并将它们放在全局命名空间中的单个对象上。 这是对多脚本方法的改进,但仍然存在将至少一个对象放在全局命名空间中的相同问题,并且没有使第三方之间持续共享代码的问题变得更容易。
之后,出现了一些模块解决方案:CommonJS,一种在Node.js中实现的同步方法,异步模块定义(AMD),这是一个异步方法和 通用模块定义 (UMD),它旨在成为一种支持以前两种样式的通用方法。
这些解决方案的出现使开发人员更容易以 包 的形式共享和重用代码,这些模块可以分发和共享,例如 npm 上的模块。 然而,由于有许多解决方案并且没有一个是 JavaScript 原生的,因此必须实现诸如 Babel、Webpack 或 Browserify 之类的工具才能在浏览器中使用模块。
由于多文件方法的许多问题以及所提出的解决方案的复杂性,开发人员有兴趣将 模块化编程 方法引入 JavaScript 语言。 因此,ECMAScript 2015 支持使用 JavaScript 模块。
module 是一组代码,它充当接口,为其他模块提供功能,并能够依赖其他模块的功能。 模块 导出 以提供代码, 导入 以使用其他代码。 模块很有用,因为它们允许开发人员重用代码,它们提供了许多开发人员可以使用的稳定、一致的接口,并且它们不会污染全局命名空间。
模块(有时称为 ECMAScript 模块或 ES 模块)现在可以在 JavaScript 中本地使用,在本教程的其余部分中,您将探索如何在代码中使用和实现它们。
原生 JavaScript 模块
JavaScript 中的模块使用 import
和 export
关键字:
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
标记。 任何使用 import
或 export
的代码都必须使用此属性:
索引.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 等捆绑器一起使用,以增加对浏览器的支持和附加功能,但它们也可以直接在浏览器中使用。
接下来,您将探索使用 import
和 export
语法的更多方式。
命名导出
如前所述,使用 export
语法将允许您单独导入已按名称导出的值。 例如,以 functions.js
的这个简化版本为例:
函数.js
export function sum() {} export function difference() {}
这将允许您使用花括号按名称导入 sum
和 difference
:
脚本.js
import { sum, difference } from './functions.js'
也可以使用别名来重命名函数。 您可以这样做以避免同一模块内的命名冲突。 在本例中,sum
将重命名为 add
,difference
将重命名为 subtract
。
脚本.js
import { sum as add, difference as subtract } from './functions.js' add(1, 2) // 3
在此处调用 add()
将产生 sum()
函数的结果。
使用 *
语法,您可以将整个模块的内容导入到一个对象中。 在这种情况下,sum
和 difference
将成为 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
现在脚本可以使用默认值和命名值。
结论
模块化编程设计实践允许您将代码分成单独的组件,这有助于使您的代码可重用和一致,同时还可以保护全局命名空间。 可以使用 import
和 export
关键字在原生 JavaScript 中实现模块接口。
在本文中,您了解了 JavaScript 中模块的历史,如何将 JavaScript 文件分成多个顶级脚本,如何使用模块化方法更新这些文件,以及 import
和 [X217X ] 命名和默认导出的语法。
要了解有关 JavaScript 模块的更多信息,请阅读 Mozilla 开发人员网络上的 Modules。 如果您想探索 Node.js 中的模块,请尝试我们的 如何创建 Node.js 模块教程 。