系列文章目录
《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 基本语法](#二、ESM 基本语法)
-
- [2.1 导出](#2.1 导出)
- [2.2 导入](#2.2 导入)
- [2.3 静态 vs 动态](#2.3 静态 vs 动态)
- [三、ESM vs CommonJS(面试高频)](#三、ESM vs CommonJS(面试高频))
-
- [3.1 经典对比:实时绑定 vs 值拷贝](#3.1 经典对比:实时绑定 vs 值拷贝)
- [3.2 互操作(了解)](#3.2 互操作(了解))
- [四、Tree Shaking 前提](#四、Tree Shaking 前提)
- 五、循环依赖
- 六、浏览器与构建工具中的模块
-
- [6.1 浏览器原生 ESM](#6.1 浏览器原生 ESM)
- [6.2 Vite / Webpack 角色(一句话)](#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 导出
javascript
/* 命名导出 */
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 导入
javascript
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 动态
静态 import |
动态 import() |
|
|---|---|---|
| 位置 | 必须在顶层 (不能写在 if 里) |
可在任意表达式位置 |
| 时机 | 编译阶段 确定依赖图 | 运行时 返回 Promise |
| 用途 | 常规依赖 | 代码分割、按路由/条件加载 |
javascript
/* 动态 import --- 打包器会拆成独立 chunk */
button.addEventListener("click", async () => {
const { renderChart } = await import("./chart.js");
renderChart();
});
三、ESM vs CommonJS(面试高频)
| ESM | CJS(Node 传统) | |
|---|---|---|
| 语法 | import / export |
require() / module.exports |
| 加载 | 编译时 静态分析 | 运行时 同步 require |
| 导出绑定 | 实时绑定(live binding) | 值拷贝(基本类型/对象引用快照) |
| 动态加载 | import() 返回 Promise |
require() 可写在 if 里 |
| Tree Shaking | ✅ 前提 | ❌ 难静态分析 |
3.1 经典对比:实时绑定 vs 值拷贝
javascript
/* 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 是数字快照 */
javascript
/* 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 :打包时去掉 未被引用的导出,减小体积。
要能摇树,通常需要:
- 使用 ESM (
import/export),依赖图在 构建时 可分析。 - 避免把 ESM 整包转成 CJS (Babel
modules: false或@babel/preset-env保留 ESM 给打包器处理)。 package.json的sideEffects:声明哪些文件有副作用(如全局 CSS、改原型),无副作用可标false,打包器才敢删「看似未引用」的模块。
json
{
"sideEffects": false
}
或:
json
{
"sideEffects": ["*.css", "./src/polyfill.js"]
}
javascript
/* 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 会 先创建未完成的绑定,再执行模块体。
javascript
/* 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
html
<script type="module" src="./main.js"></script>
- 默认 defer,按依赖图加载。
- 严格模式、单例执行(同一 URL 只执行一次)。
- 跨域脚本需 CORS (
type="module")。
6.2 Vite / Webpack 角色(一句话)
- 开发:Vite 利用浏览器原生 ESM + 按需编译;Webpack 多走打包图。
- 生产 :通常仍 打包合并 ;ESM 语法经工具链输出为兼容格式,但 尽量保留静态结构 以利摇树。
七、易混淆点归纳
export default与命名导出 :default 导入名可随意;命名导出必须用{}且名字一致(或用as)。- 静态
import会提升:依赖模块先于当前模块体执行(在依赖图拓扑序下)。 import()不是宏任务面试考点 ,但返回 Promise,常用于懒加载。- CJS 的
require不能 写在 ESM 文件顶层;Node ESM 里用createRequire(import.meta.url)。 - Tree Shaking ≠ 代码分割 :摇树删死代码;
import()拆 chunk,二者互补(下篇 webpack 会展开)。 - 副作用模块 (改全局、注入样式)即使用不到也可能被打进包,需
sideEffects标注。
八、思考与练习
1. 为什么 CommonJS 难以做 Tree Shaking?
解析:require 路径可动态、加载在 运行时,打包器难以静态确定哪些导出被使用。
2. 下面代码合法吗?为什么?
javascript
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)。