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

作者选择了COVID-19 救济基金来接受捐赠,作为Write for DOnations计划的一部分。

介绍

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

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

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

模块化编程

在 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 将标题中显示变量的值,x以及yh2以下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以显示您的网站和所有结果:

使用值 10 和 5 以及 functions.js 操作的结果呈现 HTML。

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

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

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

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

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

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

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

模块(有时称为 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,您将使用importfunctions.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"script的标签index.html使用importexport必须使用此属性的任何代码

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

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

Output
Access 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 等打包器一起使用,以增加浏览器支持和附加功能,但它们也可以直接在浏览器中使用。

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

命名导出

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

函数.js
export function sum() {}
export function difference() {}

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

脚本.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()函数的结果

使用该*语法,您可以将整个模块的内容导入到一个对象中。在这种情况下,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文件分成多个顶级脚本,如何更新采用模块化方法这些文件,并importexport语法命名和默认出口。

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

觉得文章有用?

点个广告表达一下你的爱意吧 !😁