先记住这个答案
export const 和 export let 都会建立实时绑定,导入方读到的始终是源模块变量的当前值。区别在于 const 声明的变量不能被重新赋值,所以它的值在初始化之后就不会变了——导入方每次读到的都一样。let 声明的变量可以在源模块内部被重新赋值,每次赋值后导入方立刻就能读到新值。
const导出后源模块不能重新赋值,值不会变let导出后源模块可以改,导入方实时看到新值- 不管用哪种声明,导入方都不能赋值
const导出对象时,对象属性仍然可以被修改
用代码看清区别
两个模块分别用 const 和 let 导出一个计数器,然后在源模块内部尝试修改。
const 和 let 导出的不同行为JavaScript// config.mjs
export const VERSION = '1.0'
export let count = 0
export function increment() {
// VERSION = '2.0' // 报 TypeError,const 不能重新赋值
count++ // 正常,let 可以重新赋值
}
// main.mjs
// import { VERSION, count, increment } from './config.mjs'
// console.log(count) // 0
// increment()
// console.log(count) // 1 —— 实时绑定,读到了新值
// console.log(VERSION) // '1.0' —— const 不会变count 用 let 声明,源模块调用 increment() 后 count 变成了 1,导入方立刻就能读到。VERSION 用 const 声明,源模块自己都改不了,导入方自然也永远看到 '1.0'。
为什么大多数导出都用 const
实际项目里,绝大多数导出用 const 就够了。函数声明和类声明导出后行为和 const 一样——你不能在源模块里把导出的函数名重新赋值给另一个函数。
用 let 导出意味着允许源模块在运行过程中改变这个变量的值,其他所有导入方都会同步感知到。这是一种跨模块的共享可变状态,用得多了代码会很难追踪——你不知道 count 什么时候被谁改了。所以只有确实需要模块间共享状态的场景才用 let 导出。
比较典型的场景有两个。一是热更新配置:配置模块用 let 导出当前配置对象,接收到新配置时重新赋值,所有导入方立刻生效。二是状态管理的底层实现:一些轻量状态管理库利用 ESM 实时绑定来通知视图层更新。
const 导出对象时的陷阱
const 只是阻止变量被重新赋值,不阻止对象内部的属性被修改。这是 const 本身的语义,和模块系统无关。
如果 export const config = { debug: false },源模块可以写 config.debug = true,导入方也会读到 true。这时候虽然 config 这个绑定没变(还是指向同一个对象),但对象的内容变了,导入方通过实时绑定读到的还是同一个对象,自然能看到属性的变化。
想让对象完全不可修改,可以用 Object.freeze()。export const config = Object.freeze({ debug: false }) 之后,任何对 config.debug 的修改都会被静默忽略(严格模式下报 TypeError)。
容易答错的地方
- 以为
const导出就没有实时绑定 - 实时绑定是 ESM 的底层机制,和
const还是let无关。const导出也是实时绑定——只是因为值不会变,实时绑定在这里"没有意义"而已。绑定本身是存在的,只是永远返回同一个值。 - 以为导入方可以给
let导出的变量赋值 - 不管源端用
const还是let,导入方的绑定都是只读的。import { count }之后,导入方写count = 10会报TypeError。只有源模块自己能改变let导出的值。
面试官还会怎么问?
export function 导出的函数和 export const fn = () => {} 有区别吗?
在实时绑定上没有区别,都不能被重新赋值。export function foo() 有函数提升,在模块内 foo 可以在声明之前使用。export const fn = () => {} 没有提升,在声明之前使用会报 ReferenceError。在导入方看来两者行为完全一样。
如果源模块用 setInterval 每秒修改 let 导出的变量,导入方能实时感知吗?
能。let 导出的变量每次被修改后,导入方下一次读取时就能看到新值。但"实时绑定"不意味着有推送机制——导入方不会收到通知,它只是在下次读取时拿到当前值。如果需要主动通知,还是要用发布订阅模式或者响应式库。
var 导出会怎样?
ESM 里 export var count = 0 也是合法的,行为和 export let 类似——var 可以被重新赋值,导入方能通过实时绑定看到新值。但现代代码很少用 var,因为 var 的函数作用域和变量提升容易引发其他问题。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。