0071. ESM
- 1. 🎯 本节内容
- 2. 🫧 评价
- 3. 💡 思维导图
- 4. 🤔 ESM 是什么?
- 5. 🆚 CommonJS vs ESM
- 6. 🤔 在浏览器或 NodeJS 环境中可以直接使用 ESM 吗?
- 7. 🤔 ESM 具有模块作用域吗?
- 8. 💻 demos.17 - ESM 模块作用域
- 9. 🤔 CommonJS 和 ESM 的模块作用域实现原理有何区别?
- 10. 🤔 浏览器端实现的 ESM 和 NodeJS 端实现的 ESM 有什么区别?
- 11. 🤔 ESM 的核心特性有哪些?
- 12. 🤔 在浏览器中如何使用 ESM?
- 13. 🤔 在 NodeJS 中如何使用 ESM?
- 14. 🤔 ESM 的导出与导入有哪些写法?
- 15. 💻 demos.1 - 命名导出与命名导入
- 16. 💻 demos.2 - 默认导出与默认导入
- 17. 💻 demos.3 - 混合导出与混合导入
- 18. 💻 demos.4 - 重命名导出与导入
- 19. 💻 demos.5 - 整体导入命名空间
- 20. 💻 demos.6 - 动态导入按需加载
- 21. 💻 demos.7 - 在浏览器中使用 ESM
- 22. 💻 demos.11 - 模块缓存机制
- 23. 💻 demos.12 - 无绑定导入(仅执行)
- 24. 💻 demos.13 - 默认导入的 4 种等价写法
- 25. 💻 demos.14 - 依赖预加载 vs 延迟加载
- 26. 💻 demos.15 - 绑定再导出(聚合导出)
- 27. 💻 demos.14 - 依赖预加载 vs 延迟加载
- 28. 💻 demos.11 - 模块缓存机制
- 29. 💻 demos.12 - 无绑定导入(仅执行)
- 30. 💻 demos.13 - 默认导入的 4 种等价写法
- 31. 🤔 为什么 ESM 是静态的并且具有实时绑定?
- 32. 🆚 ESM vs CommonJS
- 33. 🤔 与 CommonJS 互操作有哪些常见坑?
- 34. 🤔 循环依赖与顶层 await 的行为是什么?
- 35. 🔗 引用
1. 🎯 本节内容
- ESM 的基本语法导入导出
- 命名导出默认导出整体导入重命名导出聚合导出
- 动态导入按需加载
- 实时绑定静态分析树摇优化
- 浏览器与 NodeJS 中的使用方式
- CommonJS 互操作常见坑
- 循环依赖与顶层 await 的行为
- 目录实践与综合小练习
2. 🫧 评价
ESM 是前端开发必须掌握的模块化标准,在浏览器与 NodeJS 中均可原生使用。
ESM 支持静态分析与树摇优化,能够显著提升构建效果,相较于 CommonJS,ESM 具有实时绑定与更强的语法能力。
建议在所有新项目中优先使用 ESM,在旧项目中逐步迁移。
3. 💡 思维导图
4. 🤔 ESM 是什么?
ESM 即 ES Module,是 JavaScript 官方的模块化标准,同时支持浏览器与 NodeJS 环境。
5. 🆚 CommonJS vs ESM
| 特性 | CommonJS | ESM |
|---|---|---|
| 模块作用域的实现方式 | 运行时函数包装 | 语言规范,编译时处理 |
| 模块作用域的创建 | 函数作用域(闭包) | 模块环境记录 |
| 导出本质 | 值拷贝 | 变量绑定(引用) |
顶层 this | 指向 exports | undefined |
| 严格模式 | 需手动开启 | 自动开启 |
arguments | 存在(函数参数) | 不存在 |
| 模块 API | require, module | import, export |
| 循环依赖处理 | 可能得到部分导出 | 引用未初始化值 |
6. 🤔 在浏览器或 NodeJS 环境中可以直接使用 ESM 吗?
无需安装任何依赖即可直接可以使用 ESM 规范来导入、导出模块。
- 浏览器内置了 ESM 规范支持
- NodeJS 内置了 CommonJS 与 ESM 双模块系统支持
ESM 规范是官方标准,是 JS 语言内置支持的语法(可以直接使用 import 导入模块和 export 导出模块),不需要像 AMD、CMD 这些社区方案那样引入额外的依赖包。
7. 🤔 ESM 具有模块作用域吗?
在 ESM 中,每个模块拥有独立作用域,避免全局污染。这也是其他模块化规范(比如 CommonJS、AMD、CMD 等)都具备的基本特性。
8. 💻 demos.17 - ESM 模块作用域
// 1.js - 模块 1
// 每个 ESM 模块都有独立的作用域
const message = '来自模块 1 的消息'
let count = 100
function sayHello() {
console.log('Hello from 模块 1')
}
// 导出一些内容
export { count, message, sayHello }
// 模块内的变量不会污染全局
console.log('模块 1 加载完成')
console.log('模块 1 中的 message:', message)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 2.js - 模块 2
// 即使变量名相同,也不会冲突,因为有独立的模块作用域
const message = '来自模块 2 的消息' // 与模块 1 中的 message 不冲突
let count = 200 // 与模块 1 中的 count 不冲突
function sayHello() {
// 与模块 1 中的 sayHello 不冲突
console.log('Hello from 模块 2')
}
// 导入模块 1
import { count as count1, sayHello as hello1, message as msg1 } from './1.js'
console.log('模块 2 加载完成')
console.log('模块 2 中的 message:', message)
console.log('从模块 1 导入的 message:', msg1)
console.log('模块 2 中的 count:', count)
console.log('从模块 1 导入的 count:', count1)
// 调用各自的函数
sayHello() // 调用模块 2 的函数
hello1() // 调用从模块 1 导入的函数2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"type": "module"
}2
3
测试:
node 2.js
# 模块 1 加载完成
# 模块 1 中的 message: 来自模块 1 的消息
# 模块 2 加载完成
# 模块 2 中的 message: 来自模块 2 的消息
# 从模块 1 导入的 message: 来自模块 1 的消息
# 模块 2 中的 count: 200
# 从模块 1 导入的 count: 100
# Hello from 模块 2
# Hello from 模块 12
3
4
5
6
7
8
9
10
- 两个模块都有 message、count、sayHello
- 但它们互不干扰,各自独立
- 这就是 ESM 的模块作用域特性
9. 🤔 CommonJS 和 ESM 的模块作用域实现原理有何区别?
9.1. CommonJS 的模块作用域实现原理
CommonJS 通过函数包装器实现模块作用域:
// 你写的代码
const x = 1
module.exports = { x }
// Node.js 实际执行的代码
;(function (exports, require, module, __filename, __dirname) {
const x = 1
module.exports = { x }
})()2
3
4
5
6
7
8
9
Node.js 将模块代码包装在一个立即执行函数中,通过函数作用域隔离变量。
9.2. ESM 的模块作用域实现原理
ESM 是语言层面的规范,不需要函数包装器:
// 你写的代码
const x = 1
export { x }
// 引擎直接将其识别为模块作用域
// 不需要函数包装,x 不会成为全局变量2
3
4
5
6
实现机制:
- 模块记录(Module Record):每个模块有独立的模块环境记录
- 词法环境(Lexical Environment):模块有自己的词法作用域
- 实时绑定:导出的是变量绑定的引用,不是值拷贝
// 引擎内部类似这样处理(伪代码):
const moduleRecord = {
environment: new Map([['x', 1]]), // 模块的词法环境
exports: new Map([['x', () => environment.get('x')]]), // 导出的是引用
}2
3
4
5
9.3. 关键差异示例
ESM 的实时绑定
// lib.mjs
export let mutable = 1
export const update = () => {
mutable = 2
}
// main.mjs
import { mutable, update } from './lib.mjs'
console.log(mutable) // 1
update()
console.log(mutable) // 2(变化了!)2
3
4
5
6
7
8
9
10
11
CommonJS 的值拷贝
// lib.js
let mutable = 1
module.exports = {
mutable, // 这里拷贝了值 1
update: () => {
mutable = 2
}, // 但函数内部的 mutable 是闭包变量
}
// main.js
const { mutable, update } = require('./lib')
console.log(mutable) // 1
update()
console.log(mutable) // 1(没变化!)2
3
4
5
6
7
8
9
10
11
12
13
14
9.4. 底层实现差异
ESM
// 概念上的引擎实现
const moduleMap = new Map()
async function resolveModule(specifier) {
// 1. 解析模块标识符
// 2. 创建模块记录
// 3. 解析所有导入
// 4. 异步加载依赖
// 5. 实例化模块(创建作用域)
// 6. 执行代码
}2
3
4
5
6
7
8
9
10
CommonJS
// Node.js 实现
const moduleCache = {}
function require(id) {
if (moduleCache[id]) return moduleCache[id].exports
const module = { exports: {} }
const code = fs.readFileSync(id, 'utf8')
const wrapper = `(function(exports, require, module) { ${code} })`
const fn = eval(wrapper) // 创建函数作用域
fn(module.exports, require, module)
moduleCache[id] = module
return module.exports // 返回值的拷贝
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
9.5. 总结
- ESM:基于静态分析和实时绑定,每个模块有独立的、不可直接访问的模块环境
- CommonJS:基于函数作用域包裹和值拷贝,通过闭包实现作用域隔离
这种根本差异导致了它们在循环依赖、动态导入、缓存行为等方面的不同表现。
10. 🤔 浏览器端实现的 ESM 和 NodeJS 端实现的 ESM 有什么区别?
11. 🤔 ESM 的核心特性有哪些?
11.1. 支持两种导入方式
- 支持静态导入:便于构建工具(如 Rollup、Webpack、Vite)在编译阶段确定依赖关系,完成静态分析,更好地实现 Tree Shaking 优化
- 支持动态导入:可以异步、按需导入特定模块内容,便于优化加载性能
| 特性 | 静态导入 | 动态导入 |
|---|---|---|
| 语法 | import xxx from 'module' | import('module') |
| 解析时机 | 编译/构建时解析 | 运行时解析 |
| 位置限制 | 只能在模块顶层 | 可以在任何地方 |
| 路径格式 | 必须是字符串字面量 | 可以是表达式/变量 |
| 返回值 | 同步,直接获取模块 | 异步,返回 Promise |
| Tree Shaking | ✅ 支持 | ⚠️ 有限支持,效果不如静态导入 |
| 代码拆分 | ⚠️ 有限支持,效果不如动态导入 | ✅ 支持 |
11.2. 自动开启严格模式
ES6 模块默认就是严格模式,无需 'use strict'。
<!-- ❌ 传统脚本(非严格模式) -->
<script>
x = 10 // 自动创建全局变量(不好!)
delete y // 可以删除变量
arguments = 42 // 可以覆盖 arguments
</script>
<!-- ✅ ESM 模块(自动严格模式) -->
<script type="module">
x = 10 // ReferenceError: x is not defined
delete y // SyntaxError
arguments = 42 // SyntaxError
</script>2
3
4
5
6
7
8
9
10
11
12
13
11.3. 单例缓存
在 ES6 模块系统中,无论你在多少地方导入同一个模块,该模块只会被执行一次,并且它的导出会被缓存和共享。
- 同一模块只执行一次:无论导入多少次,模块代码只运行一次
- 导出结果被缓存:后续导入直接使用缓存结果
- 状态是共享的:所有导入者得到相同的导出对象
- 基于 URL 识别:相同的 URL 才会命中缓存
11.4. 实时绑定机制
导入的是不可变的绑定(immutable binding),与导出值保持动态引用关系
ESM 中的导入不是值的静态拷贝,而是与导出变量保持“实时连接”的动态绑定。
// counter.js
export let count = 0 // 使用 let 声明
export const increment = () => {
count++ // 修改导出值
}
// main.js
import { count, increment } from './counter.js'
console.log(count) // 0
increment() // 修改源值
console.log(count) // 1 ✅ 实时更新!不是 02
3
4
5
6
7
8
9
10
11
12
13
14
15
12. 🤔 在浏览器中如何使用 ESM?
- 使用
script的type属性,type="module" - 模块脚本默认异步,不会阻塞页面渲染
- 相对路径必须包含文件扩展名,例如
./utils.js - 模块中的变量不会污染全局,顶层
this为undefined
<script type="module" src="./app.js"></script>对比演示未开启模块化与开启模块化两种方式:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>未开启模块化</title>
</head>
<body>
<h1>未开启模块化</h1>
<script src="./plain.js"></script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
// plain.js - 非模块脚本
var a = 1
console.log('plain.js 加载完成')2
3
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>开启模块化</title>
</head>
<body>
<h1>开启模块化</h1>
<script type="module" src="./module/index.js"></script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
// index.js - 模块脚本
var a = 1
console.log('module/index.js 加载完成')
// 导出一个符号以强调模块语义
export const flag = true2
3
4
5
控制台测试:
- 未开启模块化,页面中输入
a可以访问,说明污染了全局,不建议 - 开启模块化,页面中输入
a会提示a is not defined,说明模块作用域隔离,是正确的引入方式
13. 🤔 在 NodeJS 中如何使用 ESM?
- 在
package.json中设置type为module,或者使用.mjs扩展名 - 使用
import与export语法进行模块化 - 对于第三方包,使用包的导出字段进行解析,例如
exports与module字段
{
"type": "module"
}2
3
- NodeJS 默认是 CommonJS,
.js按 CommonJS 解析,除非使用.mjs或设置type为module - 在 CommonJS 文件中不能使用静态
import,可以使用动态导入import() - 在 ESM 文件中不能使用
require,可通过createRequire加载 CommonJS 模块 - 在 ESM 中导入本地文件需写完整扩展名,例如
./util.js
示例 .mjs 方式启用 ESM
// calc.mjs
export function add(a, b) {
return a + b
}
// main.mjs
import { add } from './calc.mjs'
console.log(add(1, 2))2
3
4
5
6
7
8
示例在 CommonJS 中使用动态导入
// app.cjs 或普通 .js
async function run() {
const m = await import('./util.mjs')
console.log(m)
}
run()2
3
4
5
6
示例在 ESM 中加载 CommonJS 模块
// app.mjs 或开启了 type 为 module 的 .js
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const pkg = require('some-cjs')
console.log(pkg)2
3
4
5
14. 🤔 ESM 的导出与导入有哪些写法?
14.1. 导出
- 命名导出,
export const a = 1,export function f(){} - 统一导出,
export { a, b, f } - 重命名导出,
export { a as x } - 默认导出,
export default 值或标识符 - 聚合导出再导出,
export { a as x } from './mod.js'或export * from './mod.js' - 一个模块可以有多个基本导出,默认导出只能有一个
- 默认导出的等价写法,
export { 标识符 as default } - 基本导出与默认导出可以并存,默认导出通常承载核心功能,基本导出常用于辅助函数或常量
🤔 绑定再导出怎么写与何时用?
- 写法,
export * from './mod.js'与export { 标识符 } from './mod.js' - 应用场景,通过目录入口文件统一组织导出接口,方便外部只写一条导入语句
- 优点,统一出口,重组并重命名接口,减少导入路径分散
- 缺点,注意命名冲突,聚合入口会使被再导出的模块在初始化阶段全部执行一次
14.2. 导入
- 命名导入,
import { a, f } from './mod.js' - 默认导入,
import x from './mod.js' - 混合导入,
import x, { a } from './mod.js' - 整体导入命名空间,
import * as ns from './mod.js' - 动态导入按需加载,
const m = await import('./mod.js') - 无绑定导入用于执行初始化代码,
import './init.js' - 使用
*导入时必须提供命名空间标识符,例如as ns - 命名空间导入的默认导出位于
ns.default - 花括号内对应具名导入,花括号外对应默认导入
- 静态导入必须是顶层语句,在执行前会被预解析与提升;动态导入可在任意位置调用,用于依赖延迟加载
- 默认导入的变量名可自行定义,不支持
as别名语法;同时导入默认与具名成员可使用混合导入或default as
15. 💻 demos.1 - 命名导出与命名导入
// math.js - 命名导出示例
export const PI = 3.14159
export function add(a, b) {
return a + b
}
export function multiply(a, b) {
return a * b
}
// 也可以统一导出
// export { PI, add, multiply }2
3
4
5
6
7
8
9
10
11
12
13
// main.js - 命名导入示例
import { PI, add, multiply } from './math.js'
console.log('PI:', PI)
console.log('2 + 3 =', add(2, 3))
console.log('4 * 5 =', multiply(4, 5))2
3
4
5
6
{
"type": "module"
}2
3
16. 💻 demos.2 - 默认导出与默认导入
// calculator.js - 默认导出示例
export default class Calculator {
add(a, b) {
return a + b
}
subtract(a, b) {
return a - b
}
multiply(a, b) {
return a * b
}
divide(a, b) {
if (b === 0) {
throw new Error('除数不能为 0')
}
return a / b
}
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.js - 默认导入示例
import Calculator from './calculator.js'
const calc = new Calculator()
console.log('10 + 5 =', calc.add(10, 5))
console.log('10 - 5 =', calc.subtract(10, 5))
console.log('10 * 5 =', calc.multiply(10, 5))
console.log('10 / 5 =', calc.divide(10, 5))2
3
4
5
6
7
8
9
{
"type": "module"
}2
3
17. 💻 demos.3 - 混合导出与混合导入
// utils.js - 混合导出示例
export const VERSION = '1.0.0'
export function formatDate(date) {
return date.toISOString().split('T')[0]
}
// 默认导出一个配置对象
export default {
appName: 'My App',
timeout: 5000,
debug: true,
}2
3
4
5
6
7
8
9
10
11
12
13
// main.js - 混合导入示例
import config, { VERSION, formatDate } from './utils.js'
console.log('配置:', config)
console.log('版本:', VERSION)
console.log('当前日期:', formatDate(new Date()))2
3
4
5
6
{
"type": "module"
}2
3
18. 💻 demos.4 - 重命名导出与导入
// moduleA.js - 重命名导出
const userName = 'Tdahuyou'
const userAge = 26
function getUserInfo() {
return `${userName}, ${userAge}`
}
// 导出时重命名
export { userName as name, userAge as age, getUserInfo as getInfo }2
3
4
5
6
7
8
9
10
// main.js - 重命名导入
import { name, age, getInfo } from './moduleA.js'
// 导入时再次重命名
import { name as username, age as userage } from './moduleA.js'
console.log('姓名:', name)
console.log('年龄:', age)
console.log('信息:', getInfo())
console.log('重命名后:', username, userage)2
3
4
5
6
7
8
9
10
{
"type": "module"
}2
3
19. 💻 demos.5 - 整体导入命名空间
// tools.js - 整体导入示例
export const version = '2.0.0'
export function log(message) {
console.log(`[LOG]: ${message}`)
}
export function warn(message) {
console.warn(`[WARN]: ${message}`)
}
export function error(message) {
console.error(`[ERROR]: ${message}`)
}2
3
4
5
6
7
8
9
10
11
12
13
14
// main.js - 使用 * 导入所有导出
import * as logger from './tools.js'
console.log('版本:', logger.version)
logger.log('这是一条普通日志')
logger.warn('这是一条警告')
logger.error('这是一条错误')2
3
4
5
6
7
{
"type": "module"
}2
3
20. 💻 demos.6 - 动态导入按需加载
// feature.js - 需要动态加载的模块
export function heavyComputation() {
console.log('执行复杂计算...')
return Array.from({ length: 1000000 }, (_, i) => i).reduce((a, b) => a + b, 0)
}
export const config = {
mode: 'production',
cache: true,
}2
3
4
5
6
7
8
9
10
// main.js - 动态导入示例
console.log('程序启动')
// 模拟用户触发某个操作后才加载模块
setTimeout(async () => {
console.log('用户触发了某个操作,开始加载功能模块...')
try {
// 动态导入返回一个 Promise
const module = await import('./feature.js')
console.log('模块加载成功')
const result = module.heavyComputation()
console.log('计算结果:', result)
console.log('配置:', module.config)
} catch (error) {
console.error('模块加载失败:', error)
}
}, 1000)
console.log('程序继续执行其他任务...')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "module"
}2
3
21. 💻 demos.7 - 在浏览器中使用 ESM
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>浏览器中使用 ESM</title>
</head>
<body>
<h1>打开控制台查看输出</h1>
<!-- type="module" 启用 ESM 模块 -->
<script type="module" src="./app.js"></script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
// app.js - 浏览器 ESM 入口
import { greet } from './utils.js'
console.log('ESM 模块在浏览器中运行')
greet('World')2
3
4
5
// utils.js
export function greet(name) {
console.log(`Hello, ${name}!`)
}2
3
4
22. 💻 demos.11 - 模块缓存机制
重复导入同一个模块只会执行一次,后续导入使用缓存结果:
// b.js
let executeCount = 0
executeCount++
console.log(`b.js called (第 ${executeCount} 次执行)`)
export const b = 'b'
export { executeCount }2
3
4
5
6
// a.js
import { b } from './b.js'
console.log('a.js called')
export const a = 'a'2
3
4
// index.js - 演示模块缓存机制
import { a } from './a.js'
import { b, executeCount } from './b.js'
console.log('第一次导入:', { a, b, executeCount })
// ✅ 再次导入同一模块,不会重新执行
import { b as b2, executeCount as count2 } from './b.js'
console.log('第二次导入:', { b: b2, executeCount: count2 })
// ✅ 从不同路径导入,仍然使用缓存
import('./b.js').then((module) => {
console.log('动态导入:', { b: module.b, executeCount: module.executeCount })
})
// 运行结果:
// b.js called (第 1 次执行)
// a.js called
// 第一次导入: { a: 'a', b: 'b', executeCount: 1 }
// 第二次导入: { b: 'b', executeCount: 1 }
// 动态导入: { b: 'b', executeCount: 1 }2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "module"
}2
3
23. 💻 demos.12 - 无绑定导入(仅执行)
当不需要使用导出的内容,只想运行一次指定脚本,可以使用无绑定导入:
// polyfill.js - 全局初始化脚本
console.log('🔧 初始化全局配置...')
// 模拟 polyfill 注入
if (!Array.prototype.at) {
Array.prototype.at = function (index) {
return this[index >= 0 ? index : this.length + index]
}
console.log('✅ Array.prototype.at polyfill 已注入')
}
// 全局配置
globalThis.APP_CONFIG = {
version: '1.0.0',
env: 'development',
}
console.log('✅ 全局配置已设置', globalThis.APP_CONFIG)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js - 无绑定导入示例
// ✅ 仅执行模块,不导入任何内容
import './polyfill.js'
// 使用全局配置
console.log('当前环境:', globalThis.APP_CONFIG.env)
// 使用 polyfill
const arr = [1, 2, 3, 4, 5]
console.log('arr.at(-1):', arr.at(-1)) // 52
3
4
5
6
7
8
9
10
{
"type": "module"
}2
3
典型应用场景:
- 全局 polyfill 注入
- 应用初始化配置
- 副作用执行(如注册全局组件)
24. 💻 demos.13 - 默认导入的 4 种等价写法
默认导入可以与具名导入并存,也可以通过 default as 或命名空间方式访问默认导出:
// a.js - 默认导出与具名导出并存示例
export const a = 1
function method() {
console.log('this is a method.')
}
export default method // 等效:export { method as default }2
3
4
5
6
// index.js - 默认导入等价写法对比
console.log('=== 默认导入的 4 种等价写法 ===\n')
// 方式 1:分别导入
await import('./index-1.js')
console.log()
// 方式 2:混合导入
await import('./index-2.js')
console.log()
// 方式 3:default as 别名
await import('./index-3.js')
console.log()
// 方式 4:命名空间导入
await import('./index-4.js')
console.log('\n✅ 以上 4 种写法完全等价')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// index-1.js - 分别导入默认和具名
import { a } from './a.js'
import method from './a.js'
console.log('方式 1 - 分别导入:')
console.log('a =', a)
method()2
3
4
5
6
7
// index-2.js - 混合导入
import method, { a } from './a.js'
console.log('方式 2 - 混合导入:')
console.log('a =', a)
method()2
3
4
5
6
// index-3.js - 使用 default as 别名
import { default as method, a } from './a.js'
console.log('方式 3 - default as 别名:')
console.log('a =', a)
method()2
3
4
5
6
// index-4.js - 命名空间导入
import * as moduleA from './a.js'
console.log('方式 4 - 命名空间导入:')
console.log('a =', moduleA.a)
moduleA.default()2
3
4
5
6
{
"type": "module"
}2
3
等价写法对比:
| 写法 | 语法 | 特点 |
|---|---|---|
| 分别导入 | import { a } from './a.js'import method from './a.js' | 最直观,但需要两行 |
| 混合导入 | import method, { a } from './a.js' | 简洁,推荐使用 |
| default as | import { default as method, a } from './a.js' | 显式表达默认导出 |
| 命名空间 | import * as m from './a.js'm.default() | 访问所有导出 |
25. 💻 demos.14 - 依赖预加载 vs 延迟加载
对比表格:
| 特性 | 预加载(静态导入) | 延迟加载(动态导入) |
|---|---|---|
| 加载时机 | 解析阶段预先加载 | 运行时按需加载 |
| 依赖关系 | 清晰可分析,便于树摇 | 难以静态分析,更灵活 |
| 使用场景 | 基础依赖、公共工具 | 大型应用按需优化首屏 |
| 性能影响 | 初始加载时间长,执行快 | 初始加载快,按需加载慢 |
| 语法 | import { a } from './mod.js' | const m = await import('./mod.js') |
交互式演示:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESM 依赖预加载与延迟加载对比</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
button {
padding: 10px 20px;
margin: 10px;
font-size: 16px;
cursor: pointer;
}
#output {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 14px;
}
</style>
</head>
<body>
<h1>ESM 依赖预加载 vs 延迟加载</h1>
<p>打开控制台查看加载过程和性能对比</p>
<div>
<button id="staticBtn">测试静态导入(预加载)</button>
<button id="dynamicBtn">测试动态导入(延迟加载)</button>
<button id="clearBtn">清空输出</button>
</div>
<div id="output"></div>
<script type="module">
const output = document.getElementById('output')
const originalLog = console.log
// 劫持 console.log 显示到页面
console.log = (...args) => {
originalLog(...args)
output.textContent += args.join(' ') + '\n'
}
document.getElementById('staticBtn').addEventListener('click', async () => {
output.textContent = ''
// 动态导入静态导入示例
await import('./index-static.js?' + Date.now())
})
document.getElementById('dynamicBtn').addEventListener('click', async () => {
output.textContent = ''
// 动态导入动态导入示例
await import('./index-dynamic.js?' + Date.now())
})
document.getElementById('clearBtn').addEventListener('click', () => {
output.textContent = ''
})
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 静态导入 - 依赖预加载
console.log('🚀 应用启动 (静态导入模式)\n')
console.time('总加载时间')
// ⚠️ 两个模块都会被预先加载,即使可能只用到其中一个
import * as dynamicModule1 from './dynamicModule1.js'
import * as dynamicModule2 from './dynamicModule2.js'
console.log('\n--- 模块加载完成,开始执行业务逻辑 ---\n')
const random = Math.random()
console.log('随机数:', random)
if (random > 0.5) {
console.log('✅ 使用模块 1')
dynamicModule1.greet()
console.log('模块 1 信息:', dynamicModule1.moduleInfo)
} else {
console.log('✅ 使用模块 2')
dynamicModule2.greet()
console.log('模块 2 信息:', dynamicModule2.moduleInfo)
}
console.timeEnd('总加载时间')
console.log('\n💡 注意:两个模块都被加载了,即使只用到一个')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 动态导入 - 依赖延迟加载
console.log('🚀 应用启动 (动态导入模式)\n')
console.time('总加载时间')
;(async () => {
console.log('--- 应用初始化完成,准备按需加载模块 ---\n')
const random = Math.random()
console.log('随机数:', random)
// ✅ 只加载需要的模块
if (random > 0.5) {
console.log('⏳ 按需加载模块 1...\n')
const mod = await import('./dynamicModule1.js')
console.log('\n✅ 使用模块 1')
mod.greet()
console.log('模块 1 信息:', mod.moduleInfo)
} else {
console.log('⏳ 按需加载模块 2...\n')
const mod = await import('./dynamicModule2.js')
console.log('\n✅ 使用模块 2')
mod.greet()
console.log('模块 2 信息:', mod.moduleInfo)
}
console.timeEnd('总加载时间')
console.log('\n💡 注意:只加载了实际使用的模块,节省了资源')
})()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 模拟大型模块初始化
console.log('📦 模块 1 开始加载...')
const startTime = performance.now()
// 模拟复杂计算
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
const loadTime = (performance.now() - startTime).toFixed(2)
console.log(`✅ 模块 1 加载完成 (耗时: ${loadTime}ms)`)
export const greet = () => {
console.log('👋 来自模块 1 的问候')
}
export const moduleInfo = {
name: 'dynamicModule1',
loadTime,
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模拟大型模块初始化
console.log('📦 模块 2 开始加载...')
const startTime = performance.now()
// 模拟复杂计算
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
const loadTime = (performance.now() - startTime).toFixed(2)
console.log(`✅ 模块 2 加载完成 (耗时: ${loadTime}ms)`)
export const greet = () => {
console.log('👋 来自模块 2 的问候')
}
export const moduleInfo = {
name: 'dynamicModule2',
loadTime,
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果对比:
# 静态导入(预加载)
📦 模块 1 开始加载...
✅ 模块 1 加载完成 (耗时: 2.5ms)
📦 模块 2 开始加载...
✅ 模块 2 加载完成 (耗时: 2.3ms)
💡 注意:两个模块都被加载了,即使只用到一个
# 动态导入(延迟加载)
📦 模块 1 开始加载...
✅ 模块 1 加载完成 (耗时: 2.4ms)
💡 注意:只加载了实际使用的模块,节省了资源2
3
4
5
6
7
8
9
10
11
26. 💻 demos.15 - 绑定再导出(聚合导出)
26.1. 场景 1:基本用法
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESM 绑定再导出 演示</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
button {
padding: 10px 20px;
margin: 10px;
font-size: 16px;
cursor: pointer;
}
#output {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 14px;
}
</style>
</head>
<body>
<h1>ESM 绑定再导出演示</h1>
<p>打开控制台查看不同场景的演示</p>
<div>
<button id="basicBtn">基本用法</button>
<button id="conflictBtn">命名冲突解决</button>
<button id="noDefaultBtn">export * 不含 default</button>
<button id="clearBtn">清空输出</button>
</div>
<div id="output"></div>
<script type="module">
const output = document.getElementById('output')
const originalLog = console.log
const originalDir = console.dir
// 劫持 console
console.log = (...args) => {
originalLog(...args)
output.textContent += args.join(' ') + '\n'
}
console.dir = (obj) => {
originalDir(obj)
output.textContent += JSON.stringify(obj, null, 2) + '\n'
}
document.getElementById('basicBtn').addEventListener('click', async () => {
output.textContent = ''
await import('./main.js?' + Date.now())
})
document.getElementById('conflictBtn').addEventListener('click', async () => {
output.textContent = ''
await import('./main-conflict.js?' + Date.now())
})
document.getElementById('noDefaultBtn').addEventListener('click', async () => {
output.textContent = ''
await import('./main-no-default.js?' + Date.now())
})
document.getElementById('clearBtn').addEventListener('click', () => {
output.textContent = ''
})
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
// main.js - 聚合导出演示
console.log('=== 聚合导出基本用法 ===\n')
import * as utils from './utils/index.js'
console.log('utils 命名空间对象:')
console.dir(utils)
console.log('\n--- 使用聚合导出的成员 ---')
console.log('add(1, 2) =', utils.add(1, 2))
console.log('getRandom(1, 10) =', utils.getRandom(1, 10))
console.log('sayHello():', utils.sayHello())
console.log('constants.A =', utils.constants.A)
console.log('直接导出的 A =', utils.A)
console.log('\n✅ 通过聚合导出,外部只需一条导入语句即可访问所有工具函数')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export { add } from './add.js'
export { default as sayHello } from './sayHello.js'
export * as constants from './constants.js'
export { A, B, C } from './constants.js'
export * from './getRandom.js'
console.log('utils/index.js called')2
3
4
5
6
export const add = (a, b) => {
return a + b
}
console.log('utils/add.js called')2
3
4
export const A = 1
export const B = 2
export const C = 3
console.log('utils/constants.js called')2
3
4
5
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min
}
export { getRandom }
console.log('utils/getRandom.js called')2
3
4
5
6
26.2. 场景 2:命名冲突解决方案
// main-conflict.js - 测试命名冲突解决方案
import { add, conflictAdd, addModule, conflictModule } from './utils/index-conflict.js'
console.log('\n=== 命名冲突解决方案演示 ===\n')
// 方案 1:重命名导出
console.log('方案 1 - 重命名导出:')
console.log('add(1, 2) =', add(1, 2)) // 3
console.log('conflictAdd(1, 2) =', conflictAdd(1, 2)) // 103
console.log('\n方案 2 - 命名空间导出:')
console.log('addModule.add(1, 2) =', addModule.add(1, 2)) // 3
console.log('conflictModule.add(1, 2) =', conflictModule.add(1, 2)) // 1032
3
4
5
6
7
8
9
10
11
12
13
// index-conflict.js - 演示命名冲突问题
// ❌ 错误示例:两个模块都导出了 add,会导致冲突
// export * from './add.js'
// export * from './conflict.js'
// SyntaxError: Duplicate export of 'add'
// ✅ 解决方案 1:重命名导出
export { add } from './add.js'
export { add as conflictAdd } from './conflict.js'
// ✅ 解决方案 2:命名空间导出
export * as addModule from './add.js'
export * as conflictModule from './conflict.js'
console.log('utils/index-conflict.js called')2
3
4
5
6
7
8
9
10
11
12
13
14
15
// conflict.js - 演示命名冲突
export const add = (a, b) => {
console.log('⚠️ 这是 conflict.js 中的 add 函数')
return a + b + 100
}
console.log('utils/conflict.js called')2
3
4
5
6
7
26.3. 场景 3:export * 不包含 default
// main-no-default.js - 测试 export * 不包含 default
import * as utils from './utils/index-no-default.js'
console.log('\n=== export * 不包含 default 演示 ===\n')
console.log('utils 对象:', utils)
console.log('utils.default:', utils.default) // undefined
// ❌ 无法访问 sayHello 的默认导出
// 因为 export * from './sayHello.js' 不会导出 default2
3
4
5
6
7
8
9
// index-no-default.js - 演示 export * 不包含 default
export * from './sayHello.js'
// ⚠️ export * 不会导出 default,需要显式导出
// 如果需要导出 default,必须使用:
// export { default as sayHello } from './sayHello.js'
console.log('utils/index-no-default.js called')2
3
4
5
6
7
⚠️ 注意
export * from './mod.js' 不会导出 default,如需导出默认导出,必须显式使用:
export { default as xxx } from './mod.js'26.4. 导入
- 命名导入,
import { a, f } from './mod.js' - 默认导入,
import x from './mod.js' - 混合导入,
import x, { a } from './mod.js' - 整体导入命名空间,
import * as ns from './mod.js' - 动态导入按需加载,
const m = await import('./mod.js') - 无绑定导入用于执行初始化代码,
import './init.js' - 使用
*导入时必须提供命名空间标识符,例如as ns - 命名空间导入的默认导出位于
ns.default - 花括号内对应具名导入,花括号外对应默认导入
- 静态导入必须是顶层语句,在执行前会被预解析与提升;动态导入可在任意位置调用,用于依赖延迟加载
- 默认导入的变量名可自行定义,不支持
as别名语法;同时导入默认与具名成员可使用混合导入或default as
27. 💻 demos.14 - 依赖预加载 vs 延迟加载
对比表格:
| 特性 | 预加载(静态导入) | 延迟加载(动态导入) |
|---|---|---|
| 加载时机 | 解析阶段预先加载 | 运行时按需加载 |
| 依赖关系 | 清晰可分析,便于树摇 | 难以静态分析,更灵活 |
| 使用场景 | 基础依赖、公共工具 | 大型应用按需优化首屏 |
| 性能影响 | 初始加载时间长,执行快 | 初始加载快,按需加载慢 |
| 语法 | import { a } from './mod.js' | const m = await import('./mod.js') |
交互式演示:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ESM 依赖预加载与延迟加载对比</title>
<style>
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
button {
padding: 10px 20px;
margin: 10px;
font-size: 16px;
cursor: pointer;
}
#output {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-top: 20px;
white-space: pre-wrap;
font-family: 'Courier New', monospace;
font-size: 14px;
}
</style>
</head>
<body>
<h1>ESM 依赖预加载 vs 延迟加载</h1>
<p>打开控制台查看加载过程和性能对比</p>
<div>
<button id="staticBtn">测试静态导入(预加载)</button>
<button id="dynamicBtn">测试动态导入(延迟加载)</button>
<button id="clearBtn">清空输出</button>
</div>
<div id="output"></div>
<script type="module">
const output = document.getElementById('output')
const originalLog = console.log
// 劫持 console.log 显示到页面
console.log = (...args) => {
originalLog(...args)
output.textContent += args.join(' ') + '\n'
}
document.getElementById('staticBtn').addEventListener('click', async () => {
output.textContent = ''
// 动态导入静态导入示例
await import('./index-static.js?' + Date.now())
})
document.getElementById('dynamicBtn').addEventListener('click', async () => {
output.textContent = ''
// 动态导入动态导入示例
await import('./index-dynamic.js?' + Date.now())
})
document.getElementById('clearBtn').addEventListener('click', () => {
output.textContent = ''
})
</script>
</body>
</html>2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 静态导入 - 依赖预加载
console.log('🚀 应用启动 (静态导入模式)\n')
console.time('总加载时间')
// ⚠️ 两个模块都会被预先加载,即使可能只用到其中一个
import * as dynamicModule1 from './dynamicModule1.js'
import * as dynamicModule2 from './dynamicModule2.js'
console.log('\n--- 模块加载完成,开始执行业务逻辑 ---\n')
const random = Math.random()
console.log('随机数:', random)
if (random > 0.5) {
console.log('✅ 使用模块 1')
dynamicModule1.greet()
console.log('模块 1 信息:', dynamicModule1.moduleInfo)
} else {
console.log('✅ 使用模块 2')
dynamicModule2.greet()
console.log('模块 2 信息:', dynamicModule2.moduleInfo)
}
console.timeEnd('总加载时间')
console.log('\n💡 注意:两个模块都被加载了,即使只用到一个')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 动态导入 - 依赖延迟加载
console.log('🚀 应用启动 (动态导入模式)\n')
console.time('总加载时间')
;(async () => {
console.log('--- 应用初始化完成,准备按需加载模块 ---\n')
const random = Math.random()
console.log('随机数:', random)
// ✅ 只加载需要的模块
if (random > 0.5) {
console.log('⏳ 按需加载模块 1...\n')
const mod = await import('./dynamicModule1.js')
console.log('\n✅ 使用模块 1')
mod.greet()
console.log('模块 1 信息:', mod.moduleInfo)
} else {
console.log('⏳ 按需加载模块 2...\n')
const mod = await import('./dynamicModule2.js')
console.log('\n✅ 使用模块 2')
mod.greet()
console.log('模块 2 信息:', mod.moduleInfo)
}
console.timeEnd('总加载时间')
console.log('\n💡 注意:只加载了实际使用的模块,节省了资源')
})()2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 模拟大型模块初始化
console.log('📦 模块 1 开始加载...')
const startTime = performance.now()
// 模拟复杂计算
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
const loadTime = (performance.now() - startTime).toFixed(2)
console.log(`✅ 模块 1 加载完成 (耗时: ${loadTime}ms)`)
export const greet = () => {
console.log('👋 来自模块 1 的问候')
}
export const moduleInfo = {
name: 'dynamicModule1',
loadTime,
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模拟大型模块初始化
console.log('📦 模块 2 开始加载...')
const startTime = performance.now()
// 模拟复杂计算
let sum = 0
for (let i = 0; i < 1000000; i++) {
sum += i
}
const loadTime = (performance.now() - startTime).toFixed(2)
console.log(`✅ 模块 2 加载完成 (耗时: ${loadTime}ms)`)
export const greet = () => {
console.log('👋 来自模块 2 的问候')
}
export const moduleInfo = {
name: 'dynamicModule2',
loadTime,
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
运行结果对比:
# 静态导入(预加载)
📦 模块 1 开始加载...
✅ 模块 1 加载完成 (耗时: 2.5ms)
📦 模块 2 开始加载...
✅ 模块 2 加载完成 (耗时: 2.3ms)
💡 注意:两个模块都被加载了,即使只用到一个
# 动态导入(延迟加载)
📦 模块 1 开始加载...
✅ 模块 1 加载完成 (耗时: 2.4ms)
💡 注意:只加载了实际使用的模块,节省了资源2
3
4
5
6
7
8
9
10
11
28. 💻 demos.11 - 模块缓存机制
重复导入同一个模块只会执行一次,后续导入使用缓存结果:
// b.js
let executeCount = 0
executeCount++
console.log(`b.js called (第 ${executeCount} 次执行)`)
export const b = 'b'
export { executeCount }2
3
4
5
6
// a.js
import { b } from './b.js'
console.log('a.js called')
export const a = 'a'2
3
4
// index.js - 演示模块缓存机制
import { a } from './a.js'
import { b, executeCount } from './b.js'
console.log('第一次导入:', { a, b, executeCount })
// ✅ 再次导入同一模块,不会重新执行
import { b as b2, executeCount as count2 } from './b.js'
console.log('第二次导入:', { b: b2, executeCount: count2 })
// ✅ 从不同路径导入,仍然使用缓存
import('./b.js').then((module) => {
console.log('动态导入:', { b: module.b, executeCount: module.executeCount })
})
// 运行结果:
// b.js called (第 1 次执行)
// a.js called
// 第一次导入: { a: 'a', b: 'b', executeCount: 1 }
// 第二次导入: { b: 'b', executeCount: 1 }
// 动态导入: { b: 'b', executeCount: 1 }2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"type": "module"
}2
3
29. 💻 demos.12 - 无绑定导入(仅执行)
当不需要使用导出的内容,只想运行一次指定脚本,可以使用无绑定导入:
// polyfill.js - 全局初始化脚本
console.log('🔧 初始化全局配置...')
// 模拟 polyfill 注入
if (!Array.prototype.at) {
Array.prototype.at = function (index) {
return this[index >= 0 ? index : this.length + index]
}
console.log('✅ Array.prototype.at polyfill 已注入')
}
// 全局配置
globalThis.APP_CONFIG = {
version: '1.0.0',
env: 'development',
}
console.log('✅ 全局配置已设置', globalThis.APP_CONFIG)2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// index.js - 无绑定导入示例
// ✅ 仅执行模块,不导入任何内容
import './polyfill.js'
// 使用全局配置
console.log('当前环境:', globalThis.APP_CONFIG.env)
// 使用 polyfill
const arr = [1, 2, 3, 4, 5]
console.log('arr.at(-1):', arr.at(-1)) // 52
3
4
5
6
7
8
9
10
{
"type": "module"
}2
3
典型应用场景:
- 全局 polyfill 注入
- 应用初始化配置
- 副作用执行(如注册全局组件)
30. 💻 demos.13 - 默认导入的 4 种等价写法
默认导入可以与具名导入并存,也可以通过 default as 或命名空间方式访问默认导出:
// a.js - 默认导出与具名导出并存示例
export const a = 1
function method() {
console.log('this is a method.')
}
export default method // 等效:export { method as default }2
3
4
5
6
// index.js - 默认导入等价写法对比
console.log('=== 默认导入的 4 种等价写法 ===\n')
// 方式 1:分别导入
await import('./index-1.js')
console.log()
// 方式 2:混合导入
await import('./index-2.js')
console.log()
// 方式 3:default as 别名
await import('./index-3.js')
console.log()
// 方式 4:命名空间导入
await import('./index-4.js')
console.log('\n✅ 以上 4 种写法完全等价')2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// index-1.js - 分别导入默认和具名
import { a } from './a.js'
import method from './a.js'
console.log('方式 1 - 分别导入:')
console.log('a =', a)
method()2
3
4
5
6
7
// index-2.js - 混合导入
import method, { a } from './a.js'
console.log('方式 2 - 混合导入:')
console.log('a =', a)
method()2
3
4
5
6
// index-3.js - 使用 default as 别名
import { default as method, a } from './a.js'
console.log('方式 3 - default as 别名:')
console.log('a =', a)
method()2
3
4
5
6
// index-4.js - 命名空间导入
import * as moduleA from './a.js'
console.log('方式 4 - 命名空间导入:')
console.log('a =', moduleA.a)
moduleA.default()2
3
4
5
6
{
"type": "module"
}2
3
等价写法对比:
| 写法 | 语法 | 特点 |
|---|---|---|
| 分别导入 | import { a } from './a.js'import method from './a.js' | 最直观,但需要两行 |
| 混合导入 | import method, { a } from './a.js' | 简洁,推荐使用 |
| default as | import { default as method, a } from './a.js' | 显式表达默认导出 |
| 命名空间 | import * as m from './a.js'm.default() | 访问所有导出 |
31. 🤔 为什么 ESM 是静态的并且具有实时绑定?
31.1. 静态分析
编译阶段即可确定依赖结构,便于构建工具进行树摇优化,删除未使用的导出
31.2. 实时绑定
导入的是对导出绑定的引用,当导出值在模块内部发生变化,导入处能够实时反映:
// counter.js
export let count = 0
export function increment() {
count++
}
// main.js
import { count, increment } from './counter.js'
console.log(count) // 0
increment()
console.log(count) // 1 ✅ 实时反映导出模块内部的变化
// ❌ 导入的绑定是只读的,不可重新赋值
// count = 10 // TypeError: Assignment to constant variable.2
3
4
5
6
7
8
9
10
11
12
13
14
15
⚠️ 注意
导入的绑定在导入方是只读的,不可重新赋值,需要通过被导入模块提供的接口进行修改
32. 🆚 ESM vs CommonJS
| 对比项 | ESM | CommonJS |
|---|---|---|
| 加载时机 | 异步加载,支持浏览器与 NodeJS | 同步加载,主要用于 NodeJS |
| 依赖分析 | 静态分析,便于树摇优化 | 运行时解析,难以树摇 |
| 导出语义 | 实时绑定,导入的是引用 | 值拷贝,导入的是快照 |
| 顶层语义 | 自动严格模式,顶层 this 为 undefined | 顶层 this 指向 exports |
| 语法能力 | 支持聚合导出、动态导入、顶层 await | 不支持语言级动态导入、顶层 await |
| 互操作 | 可通过转换或桥接与 CommonJS 互操作 | 原生使用 require 与 module.exports |
33. 🤔 与 CommonJS 互操作有哪些常见坑?
33.1. 默认导出与命名导出的对应关系
某些 CommonJS 包可能不包含默认导出,使用 import * as pkg from 'pkg' 更稳妥:
// ⚠️ 某些 CommonJS 包可能报错
import pkg from 'some-cjs-pkg'
// ✅ 更稳妥的写法
import * as pkg from 'some-cjs-pkg'
console.log(pkg.default) // 访问默认导出2
3
4
5
6
33.2. 混用 require 与 import
在 NodeJS 中混用时,需要注意文件类型与包配置,可以通过 createRequire 在 ESM 中加载 CommonJS 模块:
// app.mjs
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
// ✅ 在 ESM 中加载 CommonJS 模块
const lodash = require('lodash')2
3
4
5
6
33.3. 值拷贝 vs 实时绑定
CommonJS 的导出是对象的值拷贝,不具备实时绑定特性,与 ESM 的导入行为不同:
// counter.cjs
let count = 0
module.exports = {
count,
increment() {
count++
},
}
// main.mjs
import counter from './counter.cjs'
console.log(counter.count) // 0
counter.increment()
console.log(counter.count) // 0 ⚠️ 仍然是 0,因为是值拷贝2
3
4
5
6
7
8
9
10
11
12
13
14
15
34. 🤔 循环依赖与顶层 await 的行为是什么?
34.1. 循环依赖
ESM 的静态绑定使得彼此能够引用到对方的导出,但在初始化阶段可能是未完全赋值,需要在运行时谨慎访问:
// a.js
import { b } from './b.js'
export const a = 'a'
console.log('a.js:', b) // ⚠️ 可能是 undefined
// b.js
import { a } from './a.js'
export const b = 'b'
console.log('b.js:', a) // ⚠️ 可能是 undefined2
3
4
5
6
7
8
9
⚠️ 注意
循环依赖时,被引用的变量可能尚未初始化,建议通过函数延迟访问或重构模块结构来避免
34.2. 顶层 await
顶层 await 在 ESM 中允许使用,模块初始化将变为异步,依赖方会等待其完成,这对异步初始化很有帮助:
// config.js
const response = await fetch('/api/config')
export const config = await response.json()
// main.js
import { config } from './config.js'
// ✅ 此时 config 已经是完整的数据
console.log(config)2
3
4
5
6
7
8