Skip to content

JavaScript 模块化 ​

理解路径 ​

模块化解决的是命名隔离、依赖声明、复用和加载顺序问题。面试中不要只背规范名,要说明它们出现的运行环境:CommonJS 主要服务服务端同步文件加载,AMD/CMD 服务早期浏览器异步加载,ESM 是语言标准模块系统。

常见模块方案怎么比较? ​

方案代表加载特点典型环境
CommonJSrequire、module.exports运行时加载,导出对象可变Node.js
AMDRequireJS依赖前置,异步加载早期浏览器
CMDSeaJS就近依赖,延迟执行早期浏览器
UMD包装兼容层同时兼容多种模块环境类库分发
ESMimport、export静态链接、实时绑定、支持异步加载浏览器和现代 Node.js

ESM 有哪些核心特征? ​

  1. 顶层 import / export 是静态结构,便于提前分析依赖。
  2. 导入的是实时绑定,不是值拷贝。
  3. 模块顶层默认严格模式。
  4. 每个模块有自己的模块作用域。
  5. 浏览器中 <script type="module"> 默认 defer。
  6. 可以使用 import() 做动态导入,返回 Promise。
js
// counter.js
export let count = 0
export function inc() {
  count += 1
}

// app.js
import { count, inc } from './counter.js'
inc()
console.log(count) // 1

CommonJS 和 ESM 有什么区别? ​

维度CommonJSES Module
语法阶段普通运行时代码模块链接阶段静态分析
加载方式通常同步浏览器中异步,支持静态链接
导出语义导出对象值实时绑定
Tree shaking难静态分析更友好
循环依赖返回当前已执行的导出对象创建绑定后再执行模块
顶层 thisNode CJS 中不是 undefinedundefined

module.exports 和 exports 是什么关系? ​

在 CommonJS 中,exports 初始只是 module.exports 的引用。给 exports.foo 赋值有效;直接把 exports 重新赋值不会改变真正导出的对象。

js
exports.name = 'Ada'          // 有效
module.exports = function () {} // 有效
exports = function () {}      // 无效,只改了局部变量

默认导出和命名导出怎么选? ​

命名导出适合工具集合和多个稳定 API;默认导出适合一个模块只有一个主要产物。大项目中命名导出更利于重构、自动导入和静态检查。

动态导入有什么作用? ​

import(specifier) 返回 Promise,适合路由级拆包、按需加载、特性分支和降低首屏体积。

js
async function openEditor() {
  const { createEditor } = await import('./editor.js')
  return createEditor()
}

循环依赖如何处理? ​

循环依赖不是一定错误,但容易遇到初始化顺序问题。ESM 会先创建绑定再执行模块,因此能在一定程度上处理循环依赖;如果在依赖尚未初始化时读取,会触发暂时性死区错误或得到未完成状态。实践中应把共享常量、接口或工厂函数抽到第三个模块,减少强循环。

ESM 实时绑定 ​

js
// state.js
export let value = 1
export const setValue = (next) => {
  value = next
}

// page.js
import { value, setValue } from './state.js'

setValue(2)
console.log(value) // 2

CommonJS 导出对象 ​

js
// user.cjs
exports.name = 'Ada'
exports.rename = (name) => {
  exports.name = name
}

参考来源 ​