ES Module 是唯一真正“彻底”的解法:它从语言层面保障文件级作用域隔离,杜绝顶层变量泄漏;npm 包需声明 exports 确保模块化加载;全局挂载须收敛至带前缀的命名空间;ESLint 规则锁死隐式全局与 var。

ES Module 是唯一真正“彻底”的解法
现代 JavaScript 项目里,import/export 不是“可选项”,而是命名冲突的终结者。它从语言层面保证:每个文件默认拥有独立作用域,顶层 const、let、function 绝不会泄漏到 window 或 globalThis。
你不需要手动封装、不依赖构建工具兜底、也不靠约定——只要文件以 type="module" 加载或被打包器识别为模块,就天然隔离。
- 即使两个依赖都导出
utils,import { utils as utilsA } from 'dep-a'和import { utils as utilsB } from 'dep-b'完全互不干扰 -
import * as DepA from 'dep-a'得到的是一个命名空间对象,所有内容都在DepA下,不会和DepB撞车 - CommonJS(
require())或 IIFE 包裹的代码,在 Webpack/Vite 中会被自动转成模块沙箱,但这是构建层补救,不是语言原生保障
npm 包作者必须用 exports 字段声明入口
如果你在写供他人使用的 npm 包,光写 main 不够。不声明 exports,Node.js 和现代打包器可能 fallback 到 CommonJS,导致模块边界模糊,变量仍可能被意外提升或合并。
正确做法是在 package.json 中明确指定:
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}-
import路径走 ESM 分支,确保消费者拿到的是模块作用域代码 - 避免省略
exports后让工具猜测,某些场景下会降级为 CommonJS,破坏隔离 - 如果包只支持 ESM,可直接写
"exports": "./dist/index.mjs",强制消费端走模块流程
全局挂载必须收敛到单一命名空间 + 安全初始化
极少数场景(如 UMD 构建、浏览器内联脚本、遗留系统集成)仍需暴露全局变量。这时不能放任每个依赖都往 window 上塞名字,必须统一收敛。
安全写法不是 window.MyLib = {...},而是:
var MyLib = (typeof window !== 'undefined' ? window : globalThis).MyLib || {};
MyLib.utils = { ... };
MyLib.api = { ... };- 用
|| {}避免重复赋值报错,也防止覆盖已有属性 - 不要用
var MyLib = MyLib || {}—— 在非严格模式下,未声明变量会隐式创建全局,反而加剧污染 - 命名空间名要带业务/组织前缀(如
AcmeWidget),避开utils、core、lib这类泛用名
构建时检查:用 ESLint 锁死 var 和隐式全局
即使团队都用 import,也可能有人在调试时随手写 debugData = [...],触发隐式全局。这类问题不会报错,但会在运行时覆盖其他模块的同名变量。
启用以下 ESLint 规则能实时拦截:
-
no-implicit-globals: error—— 禁止未声明赋值(count = 1) -
no-var: error—— 强制用const/let,避免var提升污染全局 -
no-restricted-globals: ["error", "window", "globalThis"]—— 禁止直接访问全局对象,逼你走模块路径
这些规则不解决架构问题,但能堵住最常见的人为漏洞。真正的“彻底”,靠的是模块语法 + 工程约束双保险。
容易被忽略的是:模块作用域只对 JS 文件生效;CSS、JSON、HTML 模板里的全局标识符(比如内联 onclick 或 data-* 属性)仍可能撞名,这部分得靠命名规范和自动化 lint 工具单独覆盖。

















