ES6 Module 介绍
ES6 Module 是 ECMAScript 6 推出的官方原生模块化方案,用于解决 JavaScript 早期没有模块化、全局变量污染、依赖混乱、无统一标准等问题,是目前前端工程化、大型项目开发的主流模块化规范。
一、为什么需要 ES6 Module
在 ES6 Module 出现之前,JavaScript 没有语言层面的模块系统,代码只能靠一个个 <script> 标签拼接,随之而来一系列问题。
1. 全局变量污染
所有脚本共享同一个全局作用域,变量都挂到 window 上,稍不注意就会互相覆盖。
<!-- index.html:靠顺序拼接脚本 -->
<script src="./a.js"></script>
<script src="./b.js"></script>
// a.js
var count = 0;
function increment() {
count++;
}
// b.js
var count = 100; // 覆盖了 a.js 里的 count,a.js 的逻辑被破坏
2. 依赖顺序靠人工维护
脚本之间谁依赖谁无法声明,只能人为保证 <script> 的先后顺序;顺序写错,运行时就会报错或拿到 undefined。
<!-- 必须先加载 utils,再加载 main,否则 main 里找不到 formatTime -->
<script src="./utils.js"></script>
<script src="./main.js"></script>
3. 没有统一标准
社区先后出现了 IIFE、AMD、CMD、CommonJS 等方案,浏览器和 Node.js 各用各的,同一份代码往往无法通用,也缺少统一的规范约束。
4. 无法做静态优化
打包工具无法从代码中分析出哪些导出被真正使用,自然也就无法安全地剔除无用代码。
ES6 Module 正是为了一次性解决这四类问题:
| 早期问题 | ES6 Module 的解法 |
|---|---|
| 全局变量污染 | 每个模块拥有独立的模块作用域,变量默认不对外暴露 |
| 依赖顺序混乱 | 用 import / export 显式声明依赖,由引擎自动处理加载顺序 |
| 没有统一标准 | 成为 ECMAScript 官方标准,浏览器与 Node.js 通用 |
| 无法静态优化 | import / export 是静态结构,编译期即可分析,支持 Tree‑Shaking |
// counter.js:显式导出,作用域独立
export let count = 0;
export function increment() {
count++;
}
// main.js:显式导入,依赖关系一目了然,也不用再关心加载顺序
import { count, increment } from './counter.js';
increment();
console.log(count); // 1
// 即使 a.js、b.js 都定义了 count,也不会互相污染
<script type="module" src="./main.js"></script>
二、核心作用
- 实现代码拆分与复用,每个模块拥有独立作用域,避免全局变量冲突。
- 明确依赖关系,让代码结构更清晰、可维护。
- 支持静态编译,可实现 Tree‑Shaking 优化打包体积。
- 统一浏览器与 Node.js 的模块化标准,取代之前 CommonJS、AMD、CMD 等混乱局面。
三、导出方式
1. 命名导出
一个模块可导出多个变量、函数或类,导入时必须使用相同的名字。
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Circle {
constructor(r) {
this.r = r;
}
area() {
return PI * this.r ** 2;
}
}
// 也可以先在模块内声明,最后统一导出,并支持重命名
const _minus = (a, b) => a - b;
export { _minus as minus };
2. 默认导出
一个模块只能有一个默认导出,导入时可以自定义名字。
// logger.js
export default function log(msg) {
console.log(`[log] ${msg}`);
}
3. 混合导出
命名导出与默认导出可以同时存在。
// api.js
export const BASE_URL = '/api';
export function get(url) {
return fetch(BASE_URL + url);
}
export default function post(url, data) {
return fetch(BASE_URL + url, {
method: 'POST',
body: JSON.stringify(data),
});
}
// main.js —— 默认导出写在花括号外,命名导出写在花括号内
import post, { BASE_URL, get } from './api.js';
四、导入方式
1. 导入命名导出
import { a, fn } from './module.js';
支持在导入时重命名,避免命名冲突。
import { add as sum, PI } from './math.js';
console.log(sum(1, 2)); // 3
2. 导入默认导出
import defaultFn from './module.js';
3. 全部导入
把模块的所有导出收集到一个命名空间对象中。
import * as math from './math.js';
console.log(math.PI, math.add(1, 2), new math.Circle(2));
// 注意:命名空间对象是「只读」的,不能修改
math.PI = 3; // SyntaxError(严格模式下报错)
4. 只执行模块(无导入)
只想执行模块内的副作用代码,不需要导入任何变量。
import './polyfill.js';
五、ES6 Module 与 CommonJS 的核心区别
-
加载时机
- CommonJS:运行时动态加载,
require执行到哪一行才加载哪个模块。 - ES6 Module:编译时静态加载,
import在代码执行前就确定了依赖关系。
- CommonJS:运行时动态加载,
-
导出值性质
- CommonJS:导出是值的拷贝,模块内部后续的修改不会反映到导入方。
- ES6 Module:导出是值的引用(实时绑定),原模块更新,导入处同步变化。
// counter.cjs (CommonJS)
let count = 0;
function increment() {
count++;
}
// 导出的是此刻 count 的「快照」,值为 0
module.exports = { count, increment, getCount: () => count };
// main.cjs
const { count, increment, getCount } = require('./counter.cjs');
increment();
console.log(count); // 0 —— 值拷贝,外部拿到的永远是旧值
console.log(getCount()); // 1 —— 通过闭包读取内部最新值
// counter.js (ESM)
export let count = 0;
export function increment() {
count++;
}
// main.js
import { count, increment } from './counter.js';
increment();
console.log(count); // 1 —— 实时绑定,跟随原模块变化
import { count } from './counter.js';
count = 100; // TypeError —— 导入的绑定是只读的,不能直接赋值
-
严格模式
- CommonJS:默认非严格模式。
- ES6 Module:自动启用严格模式,无需手写
'use strict'。
-
顶层 this
- CommonJS:this 指向当前模块的
module.exports。 - ES6 Module:this 为
undefined。
- CommonJS:this 指向当前模块的
// CommonJS
console.log(this === module.exports); // true
// ESM
console.log(this); // undefined
- 环境与优化
- CommonJS:主要用于 Node.js,依赖运行时分析,不支持 Tree‑Shaking。
- ES6 Module:浏览器与 Node 通用,静态结构可被分析,支持 Tree‑Shaking。
// utils.js
export function used() {
return 'used';
}
export function unused() {
return 'unused';
}
// main.js
import { used } from './utils.js';
console.log(used());
// 打包后 unused 无人引用,会被 Tree‑Shaking 移除
Tree-Shaking(摇树优化)是 ES6 Module 专属的代码优化技术,核心是在打包时,自动剔除项目中未被使用的无用代码,减少打包体积、提升运行性能。
- 动态加载
- CommonJS:
require天然支持动态路径与条件加载。 - ES6 Module:静态
import必须在顶层且路径为字符串字面量,动态加载需使用import()。
- CommonJS:
// CommonJS:路径可以是变量
const plugin = require(`./plugins/${name}`);
// ESM:使用 import(),返回 Promise
async function loadChart(name) {
const module = await import(`./plugins/${name}.js`);
module.render();
}
loadChart('bar');
六、使用特点
- 模块内变量、函数默认局部作用域,不污染全局。
- 浏览器中使用需在
<script>标签添加type="module"。
<script type="module" src="./main.js"></script>
type="module" 的脚本默认具有以下行为:
-
自动延迟执行(defer),等 HTML 解析完后再执行,不会阻塞页面。
-
自动启用严格模式。
-
每个模块拥有独立作用域,顶层变量不会挂到
window上。 -
同一模块多次
import只会执行一次,结果被缓存。 -
受 CORS 限制,通过
file://直接打开会报错,需要本地服务器或正确的响应头。 -
导入路径必须完整,支持相对路径与绝对路径。
- 浏览器原生 ESM 必须写全后缀(
./utils.js),不能省略.js;Webpack / Vite 等构建工具会自动补全,所以两者行为有差异。
- 浏览器原生 ESM 必须写全后缀(
import { add } from './math.js'; // 浏览器原生:正确
import { add } from './math'; // 浏览器原生:报错,构建工具:可用
- 是现代前端工程化(Webpack/Vite/Rollup)的基础模块化规范。