前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库ESM 实时绑定与 CommonJS 区别
JSJavaScript模块与语言机制

为什么 ESM 中导出的变量是实时绑定,而 CommonJS 中通常不是?

ESM 对导出的变量建立实时绑定,导入方可读取当前值;CommonJS 则导出基础值的拷贝或对象引用,规则不同。

前端进阶之旅 · 一题精讲更新于 2026.09.05
JavaScript#模块与语言机制
先看核心答案
理解线索

实时绑定 vs 值拷贝

  1. ESM 绑定导出的变量可实时读取当前值
  2. CommonJS 拷贝基础类型复制值,对象保留引用
  3. 对象属性能变但变量绑定不随模块重赋值

导出对象时,对其属性的修改通过引用可见;但将整个变量重新赋值给新对象,导入方绑定仍指向旧对象。只有导出 let 变量本身,其重赋值才会实时反映到导入方。

核心回答

先记住这个答案

ESM 在模块实例化阶段为每个导出名称建立绑定,该绑定关联模块内部的变量(如 let 声明的变量),导入方引用该绑定,因此读取时获取模块内部的实时值。CommonJS 在模块执行时通过 module.exports 赋值,加载方拿到的是 exports 对象(引用)或基础类型值的拷贝;若导出的是基础类型,模块内部后续对其重新赋值不影响已有导入;若导出对象,则其属性变化通过同一引用可见。实时绑定是 ESM 的语言特性,CommonJS 是运行时对象赋值机制,决定了行为差异。

  • ESM 变量绑定与模块内部直接关联
  • CommonJS 导出的对象或值是被复制或持引用
  • 实时绑定依赖模块内部变量不是对象属性

为什么 ESM 绑定天然实时

ESM 模块在实例化阶段递归解析依赖,先构建模块记录(Module Record),为每个导出名称创建绑定。绑定指向模块环境的内部槽([[BindingName]]),对应模块顶层声明。当模块代码执行时,声明被实例化,绑定的值会随模块内变量更新。import 声明的标识符在整个模块作用域内是只读的,但读取它时实际上是从绑定的模块环境取当前值,因此具有实时性。

CommonJS 的模块是一个普通函数包装,模块内 module.exports 被赋值。加载方调用 require 时,拿到的是 module.exports 的当前引用,并且该引用被固定。如果模块内部重新赋值 module.exports 为新对象,已缓存的 require 仍指向旧对象。若通过 module.exports 导出一个 let 变量(其实是变量值),则 require 得到的是导出的那一刻的拷贝,因为导出值是赋给 exports 对象的属性,属性值是当前原始值快照。

计数器模块的实时访问变化

有两个模块 counter.js,分别用 ESM 和 CommonJS 实现,都导出一个 increment 函数和一个计数器 count。ESM 用 export let count = 0; export function increment(){ count++; }。CommonJS 用 let count = 0; module.exports = { count, increment(){ count++; } }。主模块每秒读取 count 一次,一共读两次并调用 increment 后打印。

在 ESM 下先引入 import { count, increment } from './counter.mjs',由于绑定存在,调用 increment() 后再次读取 count 显示为 1,是实时的。在 CommonJS 下,由于 count 是解构赋值得到的拷贝,调用 increment() 后 count 始终为 0,因为原始模块里变量变了但拷贝没变。如果改为 require('...').count 却访问的是模块对象的属性,该属性值也是初始拷贝,不会更新。只有将 count 改为对象属性如 module.exports.obj.count 才能间接看到更新。这个场景是演示实时绑定差异的经典例子。

当对象属性掩盖实时绑定

ESM 的实时绑定只对模块顶层声明的变量有效。若导出语句写的是 export default { count: 0 },导入方拿到的是默认导出的对象引用,该对象属性可以被修改且能看到,但修改模块内部 count 变量并不会改变该对象属性,因为它们是不同的存储位置。同理,export { obj } 且 obj 对象的属性改变能看到,但把 obj 整个重新赋值,导入方看到的 obj 仍指向旧对象,因为绑定指向变量本身,不是变量内容。

实际编码时,如果想让外部观察到源模块内部的状态变化,应导出顶层变量(let)或者提供 getter 函数,或者导出对象并在内部修改对象属性。如果因为性能或接口设计导出的是值语义的数据,则依赖拷贝行为。在迁移旧的 CommonJS 模块到 ESM 时,需检查内部是否有延迟赋值或动态更新,否则代码逻辑可能变化。这也是为什么许多模块同时提供函数而不是公开可变状态。

回答前,多想一步

容易答错的地方

误认为 exports 对象本身实时
module.exports 只会被求值一次,之后 require 返回缓存的对象引用。模块内部给 module.exports 重新赋值,不会影响已经缓存的对象。只有在模块内部通过修改已导出对象的属性时,外部才能看到变化。
误以为 ESM 导入的对象属性也是绑定
ESM 的实时绑定只针对变量标识符,如果导出的是对象,绑定的是对象引用,对象属性变化能否看到取决于属性访问,但绑定本身不会随内部重新赋值对象而更新。这与通过该对象 API 操作不同。
试着用自己的话回答

面试官还会怎么问?

在 ESM 中给导入的变量重新赋值会发生什么?

会抛出 TypeError,因为导入的绑定是只读的,赋值操作在运行时失败,与严格模式无关。

Node.js 中同时使用 require 和 import 是否都满足实时绑定?

只有 require 对 CommonJS 是拷贝,而 import 对 ESM 是绑定。但 Node.js 允许 import 引入 CommonJS 模块,此时导入的通常是 module.exports 的整体对象,不具备实时绑定,当作默认导入或具名导入(通过静态分析得到),行为与 require 类似。

如果 CommonJS 模块导出的是一个 object,它的属性修改能被外部看到吗?

可以。因为 module.exports 被赋值后,只要模块内部通过该对象修改属性,外部访问同一对象就能看到变化。但若直接给 module.exports 重新赋值另一个对象,外部仍指向旧对象,除非重新 require。

从一道题,走向一组知识

把知识连起来

模块与语言机制

用 const 和 let 导出的变量在实时绑定上有何区别?

ESM 中用 const 或 let 导出的变量在实时绑定效果上相同,但赋值行为不同,该题分析声明类型的影响。

模块与语言机制

import * as ns 与具名导入在实时绑定和 Tree-shaking 上有什么不同表现?

namespace 对象是否为实时绑定视图与具名导入的差异,关系到实时绑定的适用范围。

放回知识体系中复习JavaScript 进阶知识

参考资料

  • JavaScript modules

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 为什么 ESM 绑定天然实时
  3. 计数器模块的实时访问变化
  4. 当对象属性掩盖实时绑定
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑