# 模块

# 1、Node 中的模块

在 Node 中,每个文件都是一个拥有私有命名空间的独立模块。在一个文件中定义的常量、变量、函数和类对该文件而言都是私有的,除非该文件会导出它们。而被模块导出的值只有被另一个模块显式导入后才会在该模块中可见。

Node 模块使用 require() 函数导入其他模块,通过设置 Exports 对象的属性或完全替换 module.exports 对象来导出公共API。

# 2、ES6 中的模块

ES6 为 JavaScript 添加了 import 和 export 关键字

ES6 模块中的代码(与 ES6 的 class 定义中的代码类似)自动应用严格模式。

在严格模式下,在作为函数调用的函数中 this 是 undefined。而在模块中,即便在顶级代码中 this 也是 undefined(相对而言,浏览器和 Node 中的脚本都将 this 设置为全局对象)

在以原生方式使用时,ES6模块是通过特殊的 <script type="module"> 标签添加到HTML页面中的

# 2.1 ES6 模块的导出

export const PI = Math.PI;
export function test() {}
export class Atest {}

// 一次导出多个
export { PI, test, Atest }

// 默认导出
export default class Btest {}
1
2
3
4
5
6
7
8
9

# 2.2 ES6 模块的导入

导入只能出现在模块顶层,不允许在类、函数、循环或条件中出现。

// 导出默认值
import BitSet from './bitset.js';
// 导出多个值
import { a, b } from './test.js'
// 导出所有值
import * as stats from './stats.js'
// 导出默认值和其他值
import test, { a, b } from './test.js'
// 导入没有任何导出的模块
// 这样的模块会在被首次导入时运行一次(之后再导入时则什么也不做)
import './a.js'
1
2
3
4
5
6
7
8
9
10
11

导入与函数声明类似,会被“提升”到顶部,因此所有导入的值在模块代码运行时都是可用的。

在浏览器中,这个字符串会被解释为一个相对于导入模块位置的URL(在Node中,或当使用打包工具时,这个字符串会被解释为相对于当前模块的文件名,不过这在实践中没有太大差别)。模块标识符字符串必须是一个以“/”开头的绝对路径,或者是一个以“./”或“../”开头的相对路径,又或者是一个带有协议及主机名的完整URL。ES6规范不允许类似“util.js”的非限定模块标识符字符串

# 2.3 导入和导出时重命名

import { render as renderImage } from './imageutils.js'
import { render as renderUI } from './ui.js'
// 默认导入重命名
import { default as test, a, b } from './test.js'

// 重命名导出
export {
  a as testA,
  b as testB
}
1
2
3
4
5
6
7
8
9
10

# 2.4 再导出

从其他模块中导入后,再导出

import { a } from './a.js'
import { b } from './b.js'
export { a, b }

// 直接导出
export { a } from './a.js'
export { b } from './b.js'
export { c, d as testD } from './test.js'

// 导出所有
export * from './a.js'
export * from './b.js'

// 作为默认导出
export { a as default } from './a'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.5 在网页中使用 JavaScript 模块

常规脚本与模块脚本的另一个重要区别涉及跨源加载。常规 <script> 标签可以从互联网上的任何服务器加载 JavaScript 代码文件,而互联网广告、分析和追踪代码都依赖这个事实。但 <script type="module"> 增加了跨源加载的限制,即只能从包含模块的 HTML 文档所在的域加载模块,除非服务器添加了适当的 CORS 头部允许跨源加载。这个新的安全限制带来了一个副作用,就是不能在开发模式下使用 file:URL 来测试ES6模块。为此在使用 ES6 模块时,需要启动一个静态 Web 服务器来测试。

使用扩展名 .mjs 来区分模块化 JavaScript 文件和使用 .js 扩展名的常规、非模块化 JavaScript 文件。对浏览器和<script> 标签而言,文件扩展名其实无关紧要(不过,MIME 类型很重要,因此如果你使用 .mjs 文件,就需要配置 Web 服务器以跟 .js 文件相同的 MIME 类型来发送它们)。Node 对 ES6 模块的支持则依赖文件扩展名,即要靠扩展名来区分要加载的文件使用了哪种模块系统。换句话说,如果你希望自己写的 ES6 模块可以在 Node 中使用,就要考虑使用 .mjs 命名约定。

# 2.6 通过 import() 动态导入

ES2020 引入 import(),传给 import() 一个模块标识符,它就会返回一个期约对象,表示加载和运行指定模块的异步过程。动态导入完成后,这个期约会“兑现”并产生一个对象,与使用静态导入语句 import * as 得到的对象类似。

// 静态导入
import * as stats from './stats.js'
// 动态导入
import('./stats.js').then(stats => {
  // your code ... 
})
// 或者使用 async await
async function test() {
  let stats = await import('./stats.js')
}
1
2
3
4
5
6
7
8
9
10

传给 import() 的参数应该是一个模块标识符,与使用静态 import 指令时完全一样。但对于 import(),则没有使用常量字符串字面量的限制。换句话说,任何表达式只要可以求值为一个字符串且格式正确,就没问题。

动态 import() 虽然看起来像函数调用,但其实并不是。事实上,import() 是一个操作符,而圆括号则是这个操作符语法必需的部分。之所以使用如此特别的语法,是因为 import() 需要将模块标识符作为相对于当前运行模块的URL来解析,而这在实现上需要一些特殊处理,这些特殊处理在 JavaScript 函数中是不合法的。

# 2.7 import.meta.url

import.meta 这个特殊语法引用一个对象,这个对象包含当前执行模块的元数据。其中,这个对象的url属性是加载模块时使用的URL(在Node中是 file://URL )。

上次更新: 2/21/2023, 9:39:49 PM