技术知识文章集合TECHNICAL ARCHIVE · 457 DOCUMENTS

显示模式

登录
ARCHIVE DOCUMENTJS

require 和 import 的区别

所属馆藏
JavaScript
文件格式
Markdown
原始路径
JavaScript/55-require和import的区别
本文目录10 个章节
  1. 一、它们属于两套模块系统
  2. 二、不同环境中的使用方式
  3. 三、加载和执行模型不同
  4. 四、导出值、绑定和互操作
  5. 五、语法位置和执行顺序
  6. 六、是否默认严格模式
  7. 七、浏览器支持的历史说明
  8. 八、其他现代模块能力
  9. 九、常见误区总结
  10. 参考资料

requireimport 的区别

Category(分类): JavaScript Status: 已整理(2026)

原文主要从出现时间、运行环境、静态/动态、值拷贝与引用、用法、严格模式和动态导入几个角度比较 CommonJS 与 ES modules。下面尽量保留原文结构,同时把 Node.js 版本、浏览器模块脚本、CommonJS/ESM 互操作和当前导入属性补充完整。

一、它们属于两套模块系统

写法所属模块系统典型文件/环境
requiremodule.exportsexportsCommonJS(CJS)Node.js 传统模块、许多旧版 Node.js 工具链
importexportECMAScript modules(ESM)浏览器原生模块、Node.js、现代构建工具

CommonJS 最初是 Node.js 生态中使用的模块格式;ESM 是 ECMAScript 标准规定的模块格式。两者不是“同一个语法的新旧写法”,而是解析、链接、执行和互操作方式不同的两套系统。

原文提到的时间线仍可作为历史背景:CommonJS 约在 2009 年进入 Node.js 生态,ESM 在 ECMAScript 2015(ES6)中标准化。Node.js 的 ESM 支持经历过实验阶段:Node.js 12 开始可以通过 package.jsontype 使用 .js ESM,Node.js 13.2 起不再要求实验性命令行 flag。当前项目应以运行时和 package.json 配置为准,不要再添加 --experimental-modules

二、不同环境中的使用方式

2.1 浏览器

浏览器原生不认识 CommonJS 的 requireexportsmodule.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.jsontype 字段决定解析方式;没有明确配置时,具体行为还会受 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.filenameimport.meta.dirname,但为了兼容不同 Node.js 版本,库代码仍常使用 import.meta.url 的 URL 写法。

8.4 包的 exportsimports

现代 Node.js 包可以通过 package.jsonexports 限制公开入口,通过条件导出同时提供 CJS 和 ESM:

{
  "name": "example-package",
  "type": "module",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    }
  }
}

使用 exports 后,消费者不能随意访问包内未公开的文件路径。库作者需要同时测试 importrequire 两条入口,避免只在一种模块系统中可用。

九、常见误区总结

  1. requireimport 不是简单的“旧语法”和“新语法”;它们属于 CJS 与 ESM 两套模块系统;
  2. 浏览器原生不提供 CommonJS require,需要构建工具或加载器转换;
  3. ESM 不等于“所有内容编译时执行”,动态 import() 和顶层 await 仍提供异步模块能力;
  4. require 得到的是 module.exports,原始值、对象属性和重新赋值的行为不同;
  5. ESM 导入绑定只读且具有 live binding 语义,CJS 被 ESM 导入时的 named export 可能只是静态分析结果;
  6. ESM 默认严格模式,CJS 不因使用 require 自动严格;
  7. 浏览器模块相对路径通常需要扩展名,Node.js ESM 也不应依赖 CJS 的自动补扩展名;
  8. 静态 import 只能在模块顶层,条件加载使用动态 import()
  9. require 是同步调用,动态 import() 返回 Promise;
  10. 循环依赖、JSON、原生插件和顶层 await 都会影响两套模块系统的互操作。

参考资料

457 DOCUMENTS · 10 COLLECTIONS
ARCHIVE SEARCH457 篇文章

SEARCH GUIDE

输入关键词开始搜索

支持搜索文章标题、所属分类和原始文档路径。

按分类浏览

10 COLLECTIONS