
ESM 中不存在可访问的 module 对象,无法像 CommonJS 那样动态挂载属性;应通过显式声明模块级变量或闭包封装实现状态共享,避免滥用函数自身属性或全局对象。
esm 中不存在可访问的 `module` 对象,无法像 commonjs 那样动态挂载属性;应通过显式声明模块级变量或闭包封装实现状态共享,避免滥用函数自身属性或全局对象。
在 ECMAScript 模块(ESM)环境中,模块作用域是静态、不可反射的词法作用域,它不暴露为一个可读写的运行时对象(如 CommonJS 中的 module.exports 或 module)。这意味着你无法在函数体内通过 module.bar = 1 的方式向当前模块作用域“动态注入”变量——module 标识符在 ESM 中根本未定义,尝试访问会直接抛出 ReferenceError。
✅ 正确做法:显式声明 + 模块级绑定
最清晰、可维护且符合 ES 规范的方式,是在模块顶层显式声明变量或常量,并通过 export 暴露必要接口:
// math-utils.js
let bar = 1; // 模块级私有状态(可变)
const config = { version: '1.0' }; // 模块级只读配置
export function foo() {
console.log('Current bar:', bar);
bar++; // 安全修改
}
export function getBar() {
return bar;
}
export function resetBar() {
bar = 1;
}
这种方式语义明确、调试友好、支持 tree-shaking(若 bar 未被导出且无副作用引用,现代打包器可能优化掉),也完全兼容 TypeScript 类型推导。
⚠️ 不推荐的替代方案及风险
| 方案 | 示例 | 主要问题 |
|---|---|---|
| 函数自身属性 | foo.bar = 1 | ❌ 异步并发下状态污染(多个调用共享同一函数引用);❌ 无法体现模块封装意图;❌ TypeScript 中需额外声明 foo.bar: number,破坏类型安全 |
| 全局对象挂载 | globalThis.moduleState = {...} | ❌ 全局命名冲突风险高;❌ 违反模块隔离原则;❌ 服务端(Node.js)与浏览器 globalThis 行为差异导致不可移植 |
| import.meta 扩展 | import.meta.bar = 1 | ❌ 非标准用法,各环境支持不一;❌ import.meta 是只读对象(规范要求),赋值静默失败或报错 |
? 补充说明:import.meta 是 ESM 提供的唯一模块元信息对象,但它是只读的(Object.isFrozen(import.meta) === true),且设计初衷是提供 URL、上下文等元数据(如 import.meta.url),绝非模块状态存储容器。
? 进阶场景:需要私有状态 + 多函数协作?
使用立即执行函数表达式(IIFE)模式封装闭包,兼顾封装性与复用性:
// counter-manager.js
export const CounterManager = (() => {
let internalCount = 0;
return {
increment() { internalCount++; },
decrement() { internalCount--; },
getCount() { return internalCount; },
reset() { internalCount = 0; }
};
})();
// 使用
import { CounterManager } from './counter-manager.js';
CounterManager.increment();
console.log(CounterManager.getCount()); // 1
该模式将状态完全封闭在模块作用域内,对外仅暴露受控接口,既满足“避免顶层污染”的诉求,又规避了动态属性带来的不确定性。
✅ 总结建议
- 拒绝魔改:不要试图模拟 CommonJS 的 module 行为——ESM 的设计哲学是显式优于隐式;
- 优先模块变量:需要跨函数共享状态?就在模块顶层 let/const 声明,辅以 export 控制可见性;
- 警惕副作用:异步函数中修改函数自身属性(如 foo.bar)本质是共享可变状态,极易引发竞态,应改为 Promise 链式状态传递或使用 AbortController 等标准机制;
- 工具链友好:显式声明能被 ESLint(如 no-unused-vars)、TypeScript、Vite/Rollup 等工具链准确分析,保障长期可维护性。
遵循这些实践,你不仅能写出符合标准的 ESM 代码,还能让团队协作更高效、错误更易定位。










