系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充
- 第 19 篇:DOM 实战案例
- 第 20 篇:CSS 布局与可视化高频
- 第 21 篇:移动端与 viewport
- 第 22 篇:BOM 核心对象
- 第 23 篇:前端路由原理
- 第 24 篇:浏览器存储对比
- 第 25 篇:网络与跨域
- 第 26 篇:网络请求与实时通道
- 第 27 篇:Service Worker、PWA 与 Web Worker
- 第 28 篇:浏览器高级 API
- 第 29 篇:图片懒加载
- 第 30 篇:ES6+ 模块(本文)
文章目录
- 系列文章目录
- 前言
- 一、为什么需要模块
- 二、ESM 基本语法
-
- 2.1 导出
- 2.2 导入
- 2.3 静态 vs 动态
- 三、ESM vs CommonJS(面试高频)
-
- 3.1 经典对比:实时绑定 vs 值拷贝
- 3.2 互操作(了解)
- 四、Tree Shaking 前提
- 五、循环依赖
- 六、浏览器与构建工具中的模块
-
- 6.1 浏览器原生 ESM
- 6.2 Vite / Webpack 角色(一句话)
- 七、易混淆点归纳
- 八、思考与练习
- 总结
前言
第 29 篇讲完图片懒加载;本篇进入 第五阶段工程化。模块化是打包、Tree Shaking、路由懒加载的基础。本篇讲 ES Module(ESM) 的 import / export、与 CommonJS(CJS) 的差异、动态 import() 与 Tree Shaking 前提,以及循环依赖、Babel 转译等面试常考点。
一、为什么需要模块
| 全局变量污染 | 每个文件有 独立作用域 |
| 依赖关系混乱 | 显式 import 声明依赖 |
| 难以按需加载 | 动态 import() 拆 chunk |
| 打包体积大 | Tree Shaking 删未使用导出 |
现代前端默认 ESM;Node 在 "type": "module" 或 .mjs 下也以 ESM 为主,老项目仍大量 CJS。
二、ESM 基本语法
2.1 导出
/* 命名导出 */
export const PI = 3.14;
export function add(a, b) {
return a + b;
}
/* 或先定义再导出 */
const sub = (a, b) => a – b;
export { sub };
/* 默认导出(每模块最多一个) */
export default class User {
constructor(name) {
this.name = name;
}
}
2.2 导入
import User, { PI, add } from "./math.js";
import { add as plus } from "./math.js";
import * as math from "./math.js";
math.add(1, 2);
/* 仅执行模块副作用,不绑定变量 */
import "./polyfill.js";
2.3 静态 vs 动态
| 位置 | 必须在顶层(不能写在 if 里) | 可在任意表达式位置 |
| 时机 | 编译阶段 确定依赖图 | 运行时 返回 Promise |
| 用途 | 常规依赖 | 代码分割、按路由/条件加载 |
/* 动态 import — 打包器会拆成独立 chunk */
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart();
});
三、ESM vs CommonJS(面试高频)
| 语法 | import / export | require() / module.exports |
| 加载 | 编译时 静态分析 | 运行时 同步 require |
| 导出绑定 | 实时绑定(live binding) | 值拷贝(基本类型/对象引用快照) |
| 动态加载 | import() 返回 Promise | require() 可写在 if 里 |
| Tree Shaking | ✅ 前提 | ❌ 难静态分析 |
3.1 经典对比:实时绑定 vs 值拷贝
/* counter.cjs */
let n = 0;
const inc = () => ++n;
module.exports = { n, inc };
/* main.cjs */
const { n, inc } = require("./counter.cjs");
inc();
inc();
console.log(n); /* 0 — 解构时 n 是数字快照 */
/* counter.js */
export let n = 0;
export const inc = () => ++n;
/* main.js */
import { n, inc } from "./counter.js";
inc();
inc();
console.log(n); /* 2 — import 的 n 与导出模块绑定 */
3.2 互操作(了解)
- CJS → ESM:import pkg from 'cjs-pkg'(default 常为 module.exports)。
- ESM → CJS:import() 或 Node 的 createRequire。
- 混用时以 构建工具 / Node 文档 为准,面试说清 静态 ESM 利于摇树 即可。
四、Tree Shaking 前提
Tree Shaking:打包时去掉 未被引用的导出,减小体积。
要能摇树,通常需要:
{
"sideEffects": false
}
或:
{
"sideEffects": ["*.css", "./src/polyfill.js"]
}
/* utils.js */
export const add = (a, b) => a + b;
export const sub = (a, b) => a – b;
/* main.js 只用 add → 生产包中 sub 可被摇掉 */
import { add } from "./utils.js";
注意:import 'lodash' 整库引入往往 摇不干净;用 lodash-es + 按需路径或 babel-plugin-import 等。
五、循环依赖
A 引 B、B 又引 A 时,ESM 会 先创建未完成的绑定,再执行模块体。
/* a.js */
import { b } from "./b.js";
export const a = "a";
console.log("a 里读 b:", b);
/* b.js */
import { a } from "./a.js";
export const b = "b";
console.log("b 里读 a:", a);
可能出现 undefined(在对方 export 执行前就读取)。工程上应 避免循环依赖:抽公共模块、依赖倒置、事件总线解耦。
CJS 循环 require 返回 已执行部分的 exports 对象,同样容易踩坑。
六、浏览器与构建工具中的模块
6.1 浏览器原生 ESM
<script type="module" src="./main.js"></script>
- 默认 defer,按依赖图加载。
- 严格模式、单例执行(同一 URL 只执行一次)。
- 跨域脚本需 CORS(type="module")。
6.2 Vite / Webpack 角色(一句话)
- 开发:Vite 利用浏览器原生 ESM + 按需编译;Webpack 多走打包图。
- 生产:通常仍 打包合并;ESM 语法经工具链输出为兼容格式,但 尽量保留静态结构 以利摇树。
七、易混淆点归纳
八、思考与练习
1. 为什么 CommonJS 难以做 Tree Shaking?
解析:require 路径可动态、加载在 运行时,打包器难以静态确定哪些导出被使用。
2. 下面代码合法吗?为什么?
if (flag) import { a } from "./a.js";
解析:不合法;静态 import 必须在顶层。应改为 if (flag) await import("./a.js")。
3. ESM 里 import { n } 后,导出模块执行 n++,导入方 console.log(n) 是多少?
解析:若导出的是 export let n,则为 实时绑定,能读到更新后的值;CJS 解构数字则仍是快照。
4. package.json 写 "sideEffects": false 表示什么?
解析:除带 sideEffects 白名单的文件外,纯 JS 模块可安全删除未引用部分,利于 Tree Shaking。
5. 路由懒加载在 Vue / React 里底层常用什么语法?
解析:动态 import()(Vue () => import('…')、React lazy(() => import('…'))),由打包器生成独立 chunk。
总结
- ESM:静态 import / export,实时绑定,是 modern 前端与 Tree Shaking 的基础。
- CJS:require 同步、值拷贝,Node 老代码常见。
- 动态 import():运行时加载、代码分割入口。
- 工程上避免 循环依赖,Babel 避免无脑 ESM→CJS 导致摇树失效。
下一篇讲 Symbol 与 Iterator / Generator(系列第 31 篇,大纲 §31)。
