require 和 import 的区别
Category(分类): JavaScript Status: 已整理(2026)
原文主要从出现时间、运行环境、静态/动态、值拷贝与引用、用法、严格模式和动态导入几个角度比较 CommonJS 与 ES modules。下面尽量保留原文结构,同时把 Node.js 版本、浏览器模块脚本、CommonJS/ESM 互操作和当前导入属性补充完整。
一、它们属于两套模块系统
| 写法 | 所属模块系统 | 典型文件/环境 |
|---|---|---|
require、module.exports、exports | CommonJS(CJS) | Node.js 传统模块、许多旧版 Node.js 工具链 |
import、export | ECMAScript modules(ESM) | 浏览器原生模块、Node.js、现代构建工具 |
CommonJS 最初是 Node.js 生态中使用的模块格式;ESM 是 ECMAScript 标准规定的模块格式。两者不是“同一个语法的新旧写法”,而是解析、链接、执行和互操作方式不同的两套系统。
原文提到的时间线仍可作为历史背景:CommonJS 约在 2009 年进入 Node.js 生态,ESM 在 ECMAScript 2015(ES6)中标准化。Node.js 的 ESM 支持经历过实验阶段:Node.js 12 开始可以通过 package.json 的 type 使用 .js ESM,Node.js 13.2 起不再要求实验性命令行 flag。当前项目应以运行时和 package.json 配置为准,不要再添加 --experimental-modules。
二、不同环境中的使用方式
2.1 浏览器
浏览器原生不认识 CommonJS 的 require、exports 和 module.exports:
<!-- 这是浏览器原生 ESM -->
<script type="module" src="./main.js"></script>
<!-- 旧浏览器可使用打包后的降级脚本;现代浏览器会跳过 nomodule -->
<script nomodule src="./legacy-bundle.js"></script>
main.js:
import { add } from './math.js'
console.log(add(1, 2))
浏览器模块脚本的相对路径通常必须写完整文件扩展名:
import { add } from './math.js'
// 现代浏览器中通常不能省略为 './math'
浏览器加载模块时使用 URL 和 CORS 规则。直接双击 file:// HTML 文件经常导致模块加载被安全策略拦截,或者因为 MIME 类型、路径和跨源配置产生错误;开发时应通过 HTTP 服务器访问,例如:
npx serve .
# 或使用项目已有的开发服务器
这不是“ESM 永远不支持 file 协议”的绝对规范结论,而是浏览器本地文件安全策略导致的常见开发问题。
2.2 Node.js
Node.js 同时支持 CJS 和 ESM。常见的显式标记如下:
| 模块系统 | 推荐的显式标记 |
|---|---|
| CommonJS | .cjs,或最近的 package.json 设置 "type": "commonjs" |
| ESM | .mjs,或最近的 package.json 设置 "type": "module" |
如果使用 .js,Node.js 会根据最近的 package.json 的 type 字段决定解析方式;没有明确配置时,具体行为还会受 Node.js 版本和语法检测影响。团队项目最好显式声明 type,不要依赖默认值。
当前 Node.js 中:
- CJS 文件可以使用
require(); - ESM 文件可以使用静态
import和动态import(); - 动态
import()在 CJS 和 ESM 中都可用,并返回 Promise; - 较新的 Node.js 版本可以用
require()同步加载满足条件的 ESM,但含有顶层await的 ESM 不能这样加载;跨版本库仍应优先使用清晰、稳定的互操作方式。
三、加载和执行模型不同
3.1 CommonJS 的 require
在 CommonJS 中,require() 是一个运行时函数调用:
// main.cjs
const math = require('./math.cjs')
console.log(math.add(1, 2))
math.cjs:
// math.cjs
const secret = 10
function add(a, b) {
return a + b + secret
}
module.exports = {
add
}
Node.js 会解析模块路径、读取并执行模块,然后返回 module.exports。CommonJS 模块通常按解析后的文件名缓存:同一个模块再次 require 时,通常不会重新执行顶层代码。
// counter.cjs
let count = 0
count += 1
module.exports = { count }
// main.cjs
const first = require('./counter.cjs')
const second = require('./counter.cjs')
console.log(first === second) // true
console.log(first.count, second.count) // 1 1
CommonJS 的同步加载和缓存很适合 Node.js 启动阶段,但也意味着循环依赖可能得到尚未初始化完成的 exports 对象。设计模块时应尽量减少循环依赖。
3.2 ESM 的静态模块声明
静态 import 只能出现在模块顶层:
// main.mjs
import { add } from './math.mjs'
console.log(add(1, 2))
ESM 在执行模块正文之前,会先解析依赖、建立模块环境并完成链接。静态结构让运行时、编辑器和构建工具更容易进行依赖分析、tree shaking 和预加载。
“静态”并不表示所有模块内容都会在编译时执行完,也不表示 ESM 不能异步加载;它主要表示静态 import 的依赖关系必须在语法层面可分析。ESM 还支持顶层 await 和动态 import()。
3.3 动态 import()
动态导入是表达式,可以出现在函数、条件和事件处理器中,并返回 Promise:
async function openEditor() {
const module = await import('./editor.js')
module.open()
}
button.addEventListener('click', openEditor)
它适合按需加载功能,但不是把静态导入随意改写成动态导入的理由。动态导入会增加异步边界和错误处理,是否拆分应结合首屏性能、缓存和实际使用频率判断。
四、导出值、绑定和互操作
4.1 CommonJS 导出的是 module.exports
exports 最初只是指向 module.exports 的局部引用:
// utils.cjs
exports.value = 100
等价于给最初的 module.exports 对象增加属性。但如果重新给 exports 赋值,模块输出不会跟着改变:
// utils.cjs
exports.value = 200
exports = { value: 300 }
// main.cjs
const utils = require('./utils.cjs')
console.log(utils) // { value: 200 }
如果希望整个模块直接导出一个函数或类,应赋值给 module.exports:
// square.cjs
class Square {
constructor(width) {
this.width = width
}
area() {
return this.width ** 2
}
}
module.exports = Square
// main.cjs
const Square = require('./square.cjs')
const square = new Square(2)
console.log(square.area()) // 4
4.2 ESM 的 named export 和 default export
// math.mjs
export const version = '1.0.0'
export function add(a, b) {
return a + b
}
export default function multiply(a, b) {
return a * b
}
// main.mjs
import multiply, { add, version } from './math.mjs'
console.log(add(1, 2))
console.log(multiply(2, 3))
console.log(version)
- named export 使用花括号导入;
- default export 导入时可以使用任意本地名称,不使用花括号;
- 一个模块最多只能有一个 default export,但可以有多个 named export;
export * from './other.js'不会重新导出对方的 default export,需要显式写export { default } from './other.js'。
export 必须导出声明或表达式,下面是合法写法:
export const fs = 'value'
export function readFile() {}
export { readFile as read }
原文中的 export const fs 没有初始化值,会产生语法错误;如果要导出已经声明的变量,应先声明再写 export { fs }。
4.3 ESM 使用 live binding
ESM 导入的是只读的 live binding:导入方不能给绑定重新赋值,但导出方变量的后续变化可以被观察到:
// state.mjs
export let count = 0
export function increment() {
count += 1
}
// main.mjs
import { count, increment } from './state.mjs'
console.log(count) // 0
increment()
console.log(count) // 1
// count = 10 // TypeError:导入绑定不可重新赋值
这里“引用”不是指可以直接修改导入绑定,也不是简单地把一个普通对象引用复制过去;准确说法是模块绑定由导出模块维护,导入方只能读取该绑定。
CommonJS 的行为取决于导出值:
- 导出原始值时,
require()得到的是当时导出的值;之后重新给module.exports赋值,不会自动更新已经拿到的变量; - 导出对象时,多个消费者通常共享同一个对象引用,修改对象属性可能互相可见;
- 重新给
module.exports赋值和修改原对象属性是两种不同操作。
// cjs-state.cjs
module.exports = { count: 0 }
setTimeout(() => {
module.exports.count += 1
}, 10)
// cjs-main.cjs
const state = require('./cjs-state.cjs')
setTimeout(() => {
console.log(state.count) // 通常为 1:共享对象被修改
}, 20)
4.4 ESM 导入 CommonJS
Node.js 允许 ESM 导入 CommonJS。最可靠的形式是 default import,它对应 CommonJS 的完整 module.exports:
// legacy.cjs
module.exports = {
answer: 42,
getAnswer() {
return this.answer
}
}
// modern.mjs
import legacy from './legacy.cjs'
console.log(legacy.answer)
console.log(legacy.getAnswer())
Node.js 也会对 CommonJS 源码做静态分析,尝试提供 named import,但这是启发式兼容功能,不保证覆盖动态赋值,也不是 CommonJS 的 live binding:
// 可能可用,但跨工具和动态写法不应依赖
import { answer } from './legacy.cjs'
如果需要在 ESM 中使用 CJS 的 require,可以使用 createRequire:
// modern.mjs
import { createRequire } from 'node:module'
const require = createRequire(import.meta.url)
const legacy = require('./legacy.cjs')
console.log(legacy)
反过来,CJS 可以使用动态 import() 加载 ESM:
// main.cjs
async function main() {
const module = await import('./math.mjs')
console.log(module.add(1, 2))
}
main().catch(console.error)
五、语法位置和执行顺序
5.1 静态 import 只能在模块顶层
下面的代码不合法:
// 非法示意:静态 import 不能放在函数体中
function load() {
import { add } from './math.js'
}
需要条件加载时使用动态导入:
async function loadMath(shouldLoad) {
if (!shouldLoad) return null
return import('./math.js')
}
CommonJS 的 require 是普通函数调用,因此可以放在条件或函数中:
function loadMath(shouldLoad) {
if (!shouldLoad) return null
return require('./math.cjs')
}
但把 require 放进函数并不会让它自动变成异步,也可能破坏静态依赖分析;除非确实需要按条件加载,否则应在模块顶层清楚声明依赖。
5.2 ESM 的导入声明会先于模块正文建立
// state.mjs
export const value = 'exported'
// main.mjs
console.log(value) // 可以读取已建立的导入绑定
import { value } from './state.mjs'
这段代码体现的是模块实例化和绑定建立,并不是建议的代码风格。实际项目仍应把 import 放在文件顶部,以保持可读性。若在初始化完成前读取循环依赖中的 lexical binding,仍可能触发 Temporal Dead Zone 相关错误。
CommonJS 则在执行到 require() 的位置加载模块:
// common.cjs
console.log('before require')
const value = require('./value.cjs')
console.log(value)
5.3 循环依赖
两套模块都可以遇到循环依赖,但表现不同:
- CommonJS 可能把尚未执行完的
exports对象返回给另一模块; - ESM 使用模块绑定和初始化流程,过早读取尚未初始化的绑定可能抛出
ReferenceError; - 最好的解决方案通常是抽取公共模块、减少相互依赖,而不是依赖加载顺序“碰巧可用”。
六、是否默认严格模式
ESM 始终使用严格模式:
// strict.mjs
const run = () => {
mistypedVariable = 17
}
export default run
// 调用 run 时会抛 ReferenceError
CommonJS 文件默认不因为使用 require 就自动进入严格模式;它是否严格取决于代码中是否写了 'use strict'、转译工具配置或其他运行时约定:
// loose.cjs
module.exports = function () {
mistypedVariable = 17
return mistypedVariable
}
在现代项目中,仍建议显式使用严格代码、ESM 或由工具统一配置,而不要依赖非严格模式下意外创建全局变量的行为。浏览器 classic script 也不因使用 <script> 就自动严格,<script type="module"> 则是严格模式。
七、浏览器支持的历史说明
原文的浏览器版本表可以作为历史资料保留:原生 ESM 的早期支持大致从 Chrome 61、Firefox 60、Safari 10.1、Edge 16 开始。现代主流浏览器已经普遍支持模块脚本;实际兼容性应以目标浏览器和项目的构建策略为准。
CommonJS 在浏览器中仍不是原生运行时模块系统。Webpack、Rollup、esbuild、Parcel 等工具可以把 CJS 转换或打包为浏览器可执行的代码,但这不是浏览器直接实现了 require。
同样,RequireJS、Sea.js 等 AMD/加载器方案是 ESM 普及前的历史解决方案。它们仍可能出现在旧项目中,但新项目通常优先使用原生 ESM 或构建工具。
八、其他现代模块能力
8.1 node: 内置模块标识
Node.js 中推荐明确写出内置模块的 node: 前缀:
import { readFile } from 'node:fs/promises'
const path = require('node:path')
它能清楚表明依赖来自 Node.js 内置模块,并避免和用户安装的同名包混淆。
8.2 JSON 模块
Node.js 当前 ESM 语法要求为 JSON 导入提供 import attribute:
import packageConfig from './package.json' with { type: 'json' }
console.log(packageConfig.name)
浏览器是否支持 JSON module、import map 和相应属性取决于目标浏览器;不要把 Node.js 的 JSON 导入语法直接假定为所有浏览器都支持。
8.3 import.meta 与 __dirname
CJS 中常见:
console.log(__filename)
console.log(__dirname)
ESM 没有 __filename 和 __dirname。可用 URL API 获得当前模块相对资源:
// path.mjs
import { fileURLToPath } from 'node:url'
import { dirname } from 'node:path'
const filename = fileURLToPath(import.meta.url)
const directory = dirname(filename)
console.log(filename, directory)
较新的 Node.js 还提供 import.meta.filename 和 import.meta.dirname,但为了兼容不同 Node.js 版本,库代码仍常使用 import.meta.url 的 URL 写法。
8.4 包的 exports 和 imports
现代 Node.js 包可以通过 package.json 的 exports 限制公开入口,通过条件导出同时提供 CJS 和 ESM:
{
"name": "example-package",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
使用 exports 后,消费者不能随意访问包内未公开的文件路径。库作者需要同时测试 import 和 require 两条入口,避免只在一种模块系统中可用。
九、常见误区总结
require与import不是简单的“旧语法”和“新语法”;它们属于 CJS 与 ESM 两套模块系统;- 浏览器原生不提供 CommonJS
require,需要构建工具或加载器转换; - ESM 不等于“所有内容编译时执行”,动态
import()和顶层await仍提供异步模块能力; require得到的是module.exports,原始值、对象属性和重新赋值的行为不同;- ESM 导入绑定只读且具有 live binding 语义,CJS 被 ESM 导入时的 named export 可能只是静态分析结果;
- ESM 默认严格模式,CJS 不因使用
require自动严格; - 浏览器模块相对路径通常需要扩展名,Node.js ESM 也不应依赖 CJS 的自动补扩展名;
- 静态
import只能在模块顶层,条件加载使用动态import(); require是同步调用,动态import()返回 Promise;- 循环依赖、JSON、原生插件和顶层
await都会影响两套模块系统的互操作。