ES6+ 模块

系列文章目录

《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)


文章目录


前言

第 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 → ESMimport pkg from 'cjs-pkg'(default 常为 module.exports)。
  • ESM → CJSimport() 或 Node 的 createRequire
  • 混用时以 构建工具 / Node 文档 为准,面试说清 静态 ESM 利于摇树 即可。

四、Tree Shaking 前提

Tree Shaking :打包时去掉 未被引用的导出,减小体积。

要能摇树,通常需要:

  1. 使用 ESMimport / export),依赖图在 构建时 可分析。
  2. 避免把 ESM 整包转成 CJS (Babel modules: false@babel/preset-env 保留 ESM 给打包器处理)。
  3. package.jsonsideEffects :声明哪些文件有副作用(如全局 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 只执行一次)。
  • 跨域脚本需 CORStype="module")。

6.2 Vite / Webpack 角色(一句话)

  • 开发:Vite 利用浏览器原生 ESM + 按需编译;Webpack 多走打包图。
  • 生产 :通常仍 打包合并 ;ESM 语法经工具链输出为兼容格式,但 尽量保留静态结构 以利摇树。

七、易混淆点归纳

  1. export default 与命名导出 :default 导入名可随意;命名导出必须用 {} 且名字一致(或用 as)。
  2. 静态 import 会提升:依赖模块先于当前模块体执行(在依赖图拓扑序下)。
  3. import() 不是宏任务面试考点 ,但返回 Promise,常用于懒加载。
  4. CJS 的 require 不能 写在 ESM 文件顶层;Node ESM 里用 createRequire(import.meta.url)
  5. Tree Shaking ≠ 代码分割 :摇树删死代码;import() 拆 chunk,二者互补(下篇 webpack 会展开)。
  6. 副作用模块 (改全局、注入样式)即使用不到也可能被打进包,需 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 的基础。
  • CJSrequire 同步、值拷贝,Node 老代码常见。
  • 动态 import() :运行时加载、代码分割入口。
  • 工程上避免 循环依赖 ,Babel 避免无脑 ESM→CJS 导致摇树失效。

下一篇讲 Symbol 与 Iterator / Generator(系列第 31 篇,大纲 §31)。

相关推荐
xuankuxiaoyao1 小时前
阶段案例——后台管理系统
java·linux·前端
恋猫de小郭1 小时前
Android 17 内存管理将严格管控,App 要注意适配
android·前端·flutter
暗冰ཏོ1 小时前
《uni-app 跨端开发完整指南:从基础入门到 H5、小程序、App 发布上线》
前端·小程序·uni-app·vue·html5
搬砖的前端1 小时前
AI工具集:Git提交时使用AI进行CodeReview如何在前端应用构建NPM包
前端·人工智能·git·npm·codeview
Bigger2 小时前
mini-cc 终端 UI:用 React 写 CLI 是什么体验
前端·react.js·ai编程
在水一缸2 小时前
警惕供应链陷阱:从 Red Hat npm 恶意包事件看依赖安全防护
前端·安全·npm·供应链安全·red hat·恶意包·依赖安全
天下无贼!2 小时前
【功能实现】前端动态表单的实现原理与三种场景实战
前端
小雨下雨的雨2 小时前
鸿蒙PC用Electron框架 实现 房产交易系统核心算法深度解析
前端·javascript·算法·华为·electron·鸿蒙系统
snow@li2 小时前
前端:本地电脑和服务器,本质上都是一台计算机。
运维·服务器·前端