ES Modules(ESM)是JavaScript官方标准化模块系统,2015年随ES6引入,支持静态分析、单例实时绑定和动态import(),被浏览器和Node.js原生支持。

ES Modules(ESM)是 JavaScript 官方标准的模块系统,2015 年随 ES6 正式引入,如今已是现代前端开发的基石。它取代了早期 CommonJS、AMD 等非标准方案,被所有主流浏览器和 Node.js(v12+ 默认支持)原生支持。学好 ESM 不是为了“多记一个语法”,而是为了真正理解代码组织逻辑、依赖关系、打包原理,以及后续使用 Vue/React/Vite/Webpack 的底层依据。
先搞懂它为什么存在,而不是怎么写
在 ESM 出现前,JS 没有官方模块机制:全局变量污染、脚本加载顺序难控、无法静态分析依赖。ESM 的核心设计目标很明确:
- 静态可分析:import/export 必须写在顶层(不能在 if 或函数里),工具(如 Vite、Webpack)能在构建前就理清整个依赖图;
- 单例绑定 + 实时绑定:导入的是导出值的只读引用(不是拷贝),且模块实例全局唯一;
- 异步加载天然支持:动态 import() 返回 Promise,适合代码分割和按需加载。
理解这三点,比死记 import { a } from './a.js' 更重要——它解释了为什么修改导出对象属性会影响所有导入者,也解释了为什么循环引用不会报错但可能拿到 undefined。
从最简场景开始动手写
不用配置构建工具,直接用浏览器或 Node.js 运行原生 ESM:
-
浏览器中:HTML 文件里用
<script type="module">,路径必须带扩展名(./utils.js✅,./utils❌); -
Node.js 中:文件后缀改为
.mjs,或在package.json加"type": "module"; -
练基础语法组合:写一个
math.js导出加减乘除,再用默认导出、命名导出、重命名导入、批量导入(import * as math)、默认+命名混合导入各试一遍; -
故意写错验证理解:比如在 if 里写 import,看浏览器报什么错(
Illegal import declaration),加深“静态性”印象。
掌握 import 和 export 的真实行为细节
很多困惑来自对“值传递 vs 引用绑定”的模糊。试试这几个小实验:
- 导出一个对象
export const config = { theme: 'dark' },在另一个模块改config.theme = 'light',观察所有导入处是否同步更新; - 导出一个函数
export function inc() { count++ },再导出一个变量let count = 0,发现函数能改变量,但变量本身不可被外部重新赋值(const 声明限制的是绑定,不是值); - 用
export { foo as bar }和import { bar as baz }验证重命名不改变原始绑定关系; - 动态 import() 写成
const { default: MyComp } = await import('./comp.js'),感受它和静态 import 的本质区别。
进阶:结合真实开发环境理解用途
ESM 的价值在工程中才完全显现:
-
Vite 项目里:所有
.vue单文件组件的<script setup>就是 ESM 上下文,defineProps和defineEmits本质是编译期注入的 ESM 绑定; -
代码分割:路由级懒加载
const Home = () => import('./views/Home.vue')底层靠的就是动态 import(); - Tree-shaking:只有 ESM 支持真正的无副作用静态分析,Webpack/Vite 才能安全地删掉未使用的导出(CommonJS 不行);
-
类型提示:TypeScript 的
import type和export type是 ESM 语法的扩展,不产生运行时代码,却极大提升开发体验。
不复杂但容易忽略:ESM 不是“高级技巧”,它是你每天写的每个 import { ref } from 'vue' 背后的基础设施。从写清楚第一个 export default 开始,到看懂构建产物里的 __webpack_require__ 如何模拟 ESM,这条路走得越实,后续越少卡在“为什么这个包不能直接 import”。


















