模块化演进
What — 是什么
JavaScript 模块化从无到有经历了全局函数 → 命名空间 → IIFE → CommonJS → AMD/CMD → UMD → ES Module 的完整演进,ES Module(ESM)是最终的官方标准。
模块化演进时间线
| 阶段 | 年份 | 代表方案 | 核心机制 | 解决的问题 |
|---|---|---|---|---|
| 全局函数 | 1995 | 无 | 函数挂载到 window | 代码复用(最原始) |
| 命名空间 | ~2005 | YUI、Dojo | 对象挂载到 window | 减少全局污染 |
| IIFE | ~2008 | jQuery 插件 | 闭包隔离作用域 | 真正的私有变量 |
| CommonJS | 2009 | Node.js | require/exports 同步加载 | 服务端模块化 |
| AMD | 2009 | RequireJS | define/require 异步加载 | 浏览器端模块化 |
| CMD | 2011 | SeaJS | define/require 按需加载 | 浏览器端模块化(国内) |
| UMD | ~2012 | 通用模板 | 兼容 CJS + AMD + 全局 | 跨环境通用模块 |
| ES Module | 2015 | ES6 标准 | import/export 静态分析 | 官方标准,全平台统一 |
各阶段详解
1. 全局函数时代
最原始的方式:将不同功能的函数直接定义在全局作用域。
// 全局函数 — 严重污染全局命名空间
function formatData(data) { /* ... */ }
function validateForm(form) { /* ... */ }
function sendMessage(msg) { /* ... */ }
// 问题:命名冲突、无法私有化、依赖关系不明确
// 如果两个库都定义了 validate() 函数,后者覆盖前者
2. 命名空间模式
用对象包裹,减少全局变量数量,但无法真正隔离私有成员。
// 命名空间模式 — 减少了全局变量,但没有私有性
var MyApp = MyApp || {};
MyApp.Utils = {
formatData: function(data) { /* ... */ },
parseJSON: function(str) { /* ... */ }
};
MyApp.Form = {
validate: function(form) { /* ... */ },
submit: function(form) { /* ... */ }
};
// 问题:MyApp.Utils.formatData 仍然可以被外部直接修改
// MyApp.Utils.formatData = null; // 所有使用方立刻崩溃
// 内部状态完全暴露,没有封装
3. IIFE(立即执行函数表达式)
利用闭包创建私有作用域,是第一种真正实现封装的模块化方案。
// IIFE 模块模式 — 闭包隔离,暴露公共接口
var UserModule = (function() {
// 私有变量和函数(外部无法访问)
var privateList = [];
var privateId = 0;
function generateId() {
return ++privateId;
}
// 返回公共接口
return {
add: function(name) {
privateList.push({ id: generateId(), name: name });
},
getList: function() {
return privateList.slice(); // 返回副本,防止外部修改
}
};
})();
UserModule.add('Alice');
console.log(UserModule.getList()); // [{ id: 1, name: 'Alice' }]
console.log(UserModule.privateList); // undefined(私有不可访问)
console.log(privateId); // ReferenceError(私有不可访问)
IIFE 还可以接收依赖参数:
// IIFE 依赖注入模式
var MyModule = (function($, _) {
// $ 是 jQuery,_ 是 lodash
function processItems(items) {
return _.map(items, function(item) {
return $(item).text();
});
}
return { processItems: processItems };
})(jQuery, _);
4. CommonJS(CJS)
Node.js 采用的模块规范,同步加载,适合服务端文件系统。
// math.js — 导出
module.exports = {
add: function(a, b) { return a + b; },
subtract: function(a, b) { return a - b; }
};
// 或具名导出
exports.multiply = function(a, b) { return a * b; };
// app.js — 导入
const math = require('./math');
const { add } = require('./math');
console.log(math.add(1, 2)); // 3
console.log(add(3, 4)); // 7
5. AMD(Asynchronous Module Definition)
RequireJS 提出的浏览器端异步模块规范,回调方式加载。
// AMD 模块定义
define('mathModule', ['dependency1', 'dependency2'], function(dep1, dep2) {
// 模块体
function add(a, b) { return a + b; }
return {
add: add
};
});
// AMD 模块加载
require(['mathModule'], function(math) {
console.log(math.add(1, 2)); // 3
});
6. CMD(Common Module Definition)
SeaJS 提出的规范,与 AMD 的核心区别:推崇依赖就近、按需加载。
// CMD 模块定义
define(function(require, exports, module) {
// 依赖就近书写,用到时才 require
var dep1 = require('./dep1');
function add(a, b) {
var dep2 = require('./dep2'); // 按需引入
return dep2.process(a + b);
}
module.exports = { add: add };
});
CMD vs AMD 对比:
| 维度 | AMD(RequireJS) | CMD(SeaJS) |
|---|---|---|
| 依赖声明 | 前置(定义时声明所有依赖) | 就近(使用时才 require) |
| 执行时机 | 依赖全部加载后立即执行 | 依赖加载后延迟执行(按需) |
| 推崇者 | 国际社区 | 国内(玉伯) |
| 现状 | 已淘汰 | 已淘汰 |
| 代表库 | RequireJS | SeaJS |
7. UMD(Universal Module Definition)
通用模块定义,让同一段代码兼容 CJS、AMD 和全局变量三种环境。
// UMD 包装器 — 兼容 CJS + AMD + 全局变量
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD 环境
define(['dependency'], factory);
} else if (typeof module === 'object' && module.exports) {
// CommonJS 环境
module.exports = factory(require('dependency'));
} else {
// 浏览器全局变量
root.MyLibrary = factory(root.dependency);
}
})(typeof self !== 'undefined' ? self : this, function(dependency) {
// 模块实际逻辑
function myMethod() {
return dependency.doSomething();
}
return {
myMethod: myMethod
};
});
8. ES Module(ESM)
ECMAScript 2015 引入的官方模块标准,静态分析、全平台支持。
// 命名导出
export const PI = 3.14159;
export function circleArea(r) { return PI * r * r; }
export default class Shape { /* ... */ }
// 命名导入
import Shape, { PI, circleArea } from './math.js';
// 命名空间导入
import * as MathModule from './math.js';
// 重命名导入
import { circleArea as area } from './math.js';
ESM 详细特性
1. 静态分析(Static Structure)
ESM 的 import/export 语句必须出现在模块顶层,不能放在条件语句或函数中,使得打包工具能在编译时确定依赖关系。
// ESM 静态导入 — 编译时确定
import { utils } from './utils.js'; // 必须顶层,路径必须是字符串字面量
// 以下写法都是非法的:
// if (condition) { import { foo } from './bar.js'; } // SyntaxError
// const path = './' + name + '.js'; import { foo } from path; // SyntaxError
// 动态导入需要用 import() 函数
if (condition) {
const module = await import('./heavy-module.js'); // 合法
}
2. import.meta
ESM 模块的元信息对象,最常用的属性是 url(当前模块的 URL)。
// browser.js — 浏览器环境
console.log(import.meta.url); // "https://example.com/module.js"
// Node.js 中替代 __dirname
import { fileURLToPath } from 'node:url';
import { dirname } from 'node:path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
console.log(__filename); // "/project/src/module.js"
console.log(__dirname); // "/project/src"
3. 顶层 await(Top-Level Await)
ES2022 特性,允许在模块顶层直接使用 await,无需包裹在 async 函数中。
// config.js — 顶层 await 模块
const response = await fetch('/api/config');
const config = await response.json();
export default config;
// 其他模块导入此模块时会等待顶层 await 完成
import config from './config.js';
console.log(config); // 已加载完成的配置
[!warning] 顶层 await 限制 顶层 await 只能在 ESM 模块中使用。使用顶层 await 的模块会变成”异步模块”,导入它的模块也会被隐式变为异步,可能影响加载性能。
Why — 为什么
模块化解决的核心问题
1. 命名冲突
// 无模块化:全局变量冲突
// libA.js
var name = 'Library A';
function init() { console.log(name); }
// libB.js
var name = 'Library B'; // 覆盖了 libA 的 name
function init() { console.log(name); } // 覆盖了 libA 的 init
// 模块化后:各自作用域隔离
// libA.js
export const name = 'Library A';
export function init() { console.log(name); }
// libB.js
export const name = 'Library B'; // 不冲突,不同模块
export function init() { console.log(name); }
2. 依赖管理
// 无模块化:依赖靠 script 标签顺序,难以维护
// <script src="jquery.js"></script>
// <script src="lodash.js"></script>
// <script src="utils.js"></script> <!-- 依赖 jquery 和 lodash -->
// <script src="app.js"></script> <!-- 依赖 utils -->
// 顺序错了就报错,而且无法在代码中看出依赖关系
// 模块化后:依赖显式声明
import $ from 'jquery';
import _ from 'lodash';
import { formatDate } from './utils.js';
3. 代码复用
// 模块化后,一段逻辑可以跨项目复用
// shared/validators.js
export function isEmail(str) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);
}
export function isPhone(str) {
return /^1[3-9]\d{9}$/.test(str);
}
// 项目A、项目B 都可以直接 import 使用
对比方案(完整版)
| 维度 | ES Module | CommonJS | AMD | CMD | UMD | IIFE |
|---|---|---|---|---|---|---|
| 加载方式 | 静态(编译时) | 动态(运行时同步) | 动态(异步) | 动态(按需异步) | 取决于环境 | 即时执行 |
| Tree Shaking | 支持 | 不支持 | 不支持 | 不支持 | 不支持 | 不支持 |
| 浏览器支持 | 原生支持 | 不支持 | 需库 | 需库 | 需适配 | 原生 |
| Node.js 支持 | 14+ 稳定 | 原生 | 不支持 | 不支持 | 可用 | 可用 |
| 循环依赖 | 引用绑定(部分可用) | 值拷贝(可能 undefined) | 不支持 | 不支持 | 取决于环境 | 无模块依赖 |
| 异步加载 | import() | 不支持 | 回调 | 回调 | 不支持 | 不支持 |
| 私有变量 | 模块作用域 | 模块作用域 | 函数作用域 | 函数作用域 | 取决于环境 | 闭包 |
| 标准化 | ECMA 官方 | 社区规范 | 社区规范 | 社区规范 | 社区模板 | 无 |
| 现状 | 推荐使用 | 仍在使用 | 已淘汰 | 已淘汰 | 旧包使用 | 特定场景 |
优缺点
- ESM 优点:
- 静态分析,支持 Tree Shaking
- 浏览器原生支持
- 顶层 await 支持
- 导出是 live binding
- ECMA 官方标准,长期方向
import()支持动态加载
- ESM 缺点:
- 部分 npm 包只有 CJS 版本
- Node.js ESM 兼容仍有一些边界问题
- 动态条件导入不如 CJS 灵活
- 顶层 await 可能导致模块加载链变慢
- 浏览器中
import路径必须包含完整扩展名
How — 怎么用
快速上手
// ESM 导出
export const name = 'Alice';
export function greet() { return `Hello ${name}`; }
export default class User { /* ... */ }
// ESM 导入
import User, { name, greet } from './user.js';
import * as UserModule from './user.js';
// 动态导入
const module = await import('./heavy-module.js');
代码示例
示例 1:IIFE 模块模式完整示例
// iife-event-bus.js — 用 IIFE 实现发布订阅模式
var EventBus = (function() {
var events = {}; // 私有:事件监听器存储
function on(event, callback) {
if (!events[event]) events[event] = [];
events[event].push(callback);
}
function off(event, callback) {
if (!events[event]) return;
events[event] = events[event].filter(function(cb) {
return cb !== callback;
});
}
function emit(event, data) {
if (!events[event]) return;
events[event].forEach(function(cb) {
cb(data);
});
}
// 只暴露公共方法
return { on: on, off: off, emit: emit };
})();
// 使用
EventBus.on('user:login', function(user) {
console.log('Welcome, ' + user.name);
});
EventBus.emit('user:login', { name: 'Alice' });
示例 2:命名空间模式示例
// namespace-pattern.js — 命名空间模式 + 子模块
var App = App || {};
// 核心模块
App.Core = (function() {
var version = '1.0.0';
return {
getVersion: function() { return version; }
};
})();
// 工具模块
App.Utils = {
debounce: function(fn, delay) {
var timer = null;
return function() {
var context = this, args = arguments;
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
};
},
throttle: function(fn, interval) {
var lastTime = 0;
return function() {
var now = Date.now();
if (now - lastTime >= interval) {
lastTime = now;
fn.apply(this, arguments);
}
};
}
};
// 模块间通过命名空间通信
App.Core.getVersion(); // "1.0.0"
App.Utils.debounce(function() {}, 300);
示例 3:UMD 包装器写法
// umd-library.js — 发布 npm 包时常用的 UMD 模板
(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? factory(exports) // CommonJS
: typeof define === 'function' && define.amd
? define(['exports'], factory) // AMD
: (global = typeof globalThis !== 'undefined'
? globalThis : global || self,
factory(global.MyLib = {})); // 浏览器全局
})(this, function(exports) {
'use strict';
// 模块逻辑
function create(config) {
return { config: config, created: true };
}
function validate(input) {
return typeof input === 'string' && input.length > 0;
}
// 导出
exports.create = create;
exports.validate = validate;
Object.defineProperty(exports, '__esModule', { value: true });
});
示例 4:AMD define/require 完整示例
// amd-cart.js — AMD 模块定义(RequireJS 语法)
define('Cart', ['jquery', 'lodash', './storage'], function($, _, Storage) {
var items = [];
function addItem(product) {
var existing = _.find(items, { id: product.id });
if (existing) {
existing.quantity += 1;
} else {
items.push({ ...product, quantity: 1 });
}
Storage.save('cart', items);
}
function getTotal() {
return _.reduce(items, function(sum, item) {
return sum + item.price * item.quantity;
}, 0);
}
function render() {
$('#cart-list').html(
items.map(function(item) {
return '<li>' + item.name + ' x' + item.quantity + '</li>';
}).join('')
);
}
return {
addItem: addItem,
getTotal: getTotal,
render: render
};
});
// main.js — AMD 模块加载
require(['Cart'], function(Cart) {
Cart.addItem({ id: 1, name: 'Book', price: 29.9 });
Cart.addItem({ id: 2, name: 'Pen', price: 5.0 });
Cart.render();
console.log('Total:', Cart.getTotal());
});
示例 5:ESM live binding 演示
// counter.js
export let count = 0;
export function increment() { count++; }
// main.js
import { count, increment } from './counter.js';
console.log(count); // 0
increment();
console.log(count); // 1(ESM live binding,值已更新)
// 对比 CommonJS:
// const { count, increment } = require('./counter');
// console.log(count); // 0(值拷贝,永远是初始值)
示例 6:动态 import() 与代码分割实战
// 路由级代码分割
async function loadPage(route) {
switch (route) {
case '/dashboard':
const { DashboardPage } = await import('./pages/dashboard.js');
return DashboardPage;
case '/settings':
const { SettingsPage } = await import('./pages/settings.js');
return SettingsPage;
case '/profile':
const { ProfilePage } = await import('./pages/profile.js');
return ProfilePage;
default:
const { NotFound } = await import('./pages/not-found.js');
return NotFound;
}
}
// 条件加载重型依赖
async function initEditor() {
if (!document.querySelector('#editor')) return;
// 仅在需要时加载 Monaco Editor(约 2MB)
const loader = await import('monaco-editor');
const editor = loader.create(document.querySelector('#editor'), {
language: 'javascript',
theme: 'vs-dark'
});
return editor;
}
// 重试机制包装动态导入
async function importWithRetry(specifier, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await import(specifier);
} catch (err) {
if (i === retries - 1) throw err;
console.warn(`Import failed, retrying (${i + 1}/${retries})...`);
await new Promise(r => setTimeout(r, 1000 * (i + 1)));
}
}
}
示例 7:Tree Shaking 配置与验证
// utils.js — 只有 add 和 multiply 会被 Tree Shaking 保留
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// 这个函数如果没被 import,会被 Tree Shaking 移除
export function unusedHeavyFunction() {
// 大量计算逻辑...
return new Array(10000).fill(0).map((_, i) => Math.sqrt(i));
}
// main.js — 只导入了 add
import { add } from './utils.js';
console.log(add(1, 2));
// webpack.config.js — Tree Shaking 配置
module.exports = {
mode: 'production', // 生产模式自动启用 Tree Shaking
optimization: {
usedExports: true, // 标记未使用的导出
minimize: true, // 压缩代码(移除标记的未使用代码)
sideEffects: true // 读取 package.json 的 sideEffects 字段
}
};
// package.json — 声明无副作用,允许更激进的 Tree Shaking
{
"name": "my-utils",
"sideEffects": false,
"sideEffects": ["*.css", "./src/polyfill.js"]
}
// 验证 Tree Shaking 是否生效
// 构建后搜索 unusedHeavyFunction,如果没有出现则说明生效
// 注意:以下写法会导致 Tree Shaking 失效
import { add } from './utils'; // 命名导入 ✅ 可 Shaking
import _ from 'lodash'; // 默认导入整个库 ❌ 无法 Shaking
import * as Utils from './utils'; // 命名空间导入 ❌ 无法 Shaking
const { add } = require('./utils'); // CJS ❌ 无法 Shaking
示例 8:ESM 顶层 await 用法
// db-connection.js — 数据库连接模块(顶层 await)
import { MongoClient } from 'mongodb';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect(); // 顶层 await:连接数据库
const db = client.db('myapp');
export { db, client };
// app.js — 导入时自动等待连接完成
import { db } from './db-connection.js';
// 此时 db 已连接就绪,可直接使用
const users = await db.collection('users').find({}).toArray();
console.log(users);
// config-loader.js — 配置加载模块
const configPath = process.env.CONFIG_URL || '/api/config';
const response = await fetch(configPath);
if (!response.ok) {
throw new Error(`Failed to load config: ${response.status}`);
}
const config = await response.json();
// 根据配置决定导出内容
export const apiUrl = config.apiUrl;
export const features = config.features;
export default config;
// 注意:顶层 await 会让模块变为"异步模块"
// 导入它的模块也会等待,可能形成瀑布式加载
// ❌ 反模式:多个串行顶层 await
// a.js
export const dataA = await fetch('/api/a').then(r => r.json());
// b.js(依赖 a.js)
import { dataA } from './a.js'; // 等待 a.js 的 fetch 完成
export const dataB = await fetch('/api/b').then(r => r.json()); // 再 fetch b
// ✅ 更好:使用 Promise.all 并行
const [dataA, dataB] = await Promise.all([
fetch('/api/a').then(r => r.json()),
fetch('/api/b').then(r => r.json())
]);
export { dataA, dataB };
示例 9:Dual Package(同时支持 ESM 和 CJS)完整配置
// package.json — 双格式发布配置
{
"name": "my-awesome-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
},
"./utils": {
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
}
},
"files": ["dist"],
"sideEffects": false
}
// src/index.ts — 源码(TypeScript)
export function greet(name: string): string {
return `Hello, ${name}!`;
}
export const version = '1.0.0';
// tsconfig.json — 生成双格式输出
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"declaration": true,
"outDir": "./dist"
}
}
// build.js — 构建脚本(使用 esbuild 生成双格式)
import { build } from 'esbuild';
// ESM 版本
await build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'esm',
outfile: 'dist/index.mjs',
platform: 'neutral'
});
// CJS 版本
await build({
entryPoints: ['src/index.ts'],
bundle: true,
format: 'cjs',
outfile: 'dist/index.cjs',
platform: 'neutral'
});
// package.json — 使用条件导出时的"双包风险"规避
// 问题:同一个应用中如果通过 CJS 和 ESM 分别引入同一包,
// 会得到两个不同的模块实例(单例失效)
// 解决方案:ESM 入口重新导出 CJS 入口
// dist/index.mjs
import cjsModule from './index.cjs';
export const greet = cjsModule.greet;
export const version = cjsModule.version;
export default cjsModule;
示例 10:Node.js ESM 配置
// package.json
{
"type": "module",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
}
}
示例 11:CJS 与 ESM 互操作
// ESM 中导入 CJS(Node.js 自动桥接)
import pkg from 'cjs-package'; // 拿到 module.exports
const { method } = pkg;
// CJS 中导入 ESM(必须动态)
const esmModule = await import('esm-package');
// 注意事项:
// 1. ESM 中不能用 require()(除非动态 polyfill)
// 2. CJS 中不能用 import 语法(必须用 import())
// 3. ESM 导入 CJS 时,命名导出需要手动解构
// 4. CJS 的 module.exports 被视为 ESM 的 default 导出
常见问题与踩坑
| 问题 | 原因 | 解决方案 |
|---|---|---|
ESM 中 __dirname 未定义 | ESM 没有 CJS 的全局变量 | 用 import.meta.url + fileURLToPath |
| CJS 包在 Vite 中报错 | Vite 只处理 ESM | 配置 optimizeDeps.include 预构建 |
| 循环依赖 CJS 返回 undefined | CJS 值拷贝,模块未执行完 | 重构避免循环,或改用 ESM |
| 浏览器 ESM 路径必须完整 | <script type="module"> 不解析省略扩展名 | 写完整路径 ./utils.js,不能用 ./utils |
| ESM 顶层 await 阻塞后续模块 | 顶层 await 让模块变异步,后续模块必须等待 | 避免在频繁导入的模块中使用顶层 await |
| Dual Package 单例失效 | CJS 和 ESM 加载同一包产生两个实例 | ESM 入口 re-export CJS 入口,共享同一个实例 |
import 语句被提升 | ESM 的 import 是静态提升的,无论写在哪都在模块顶部执行 | 理解提升机制,不要在 import 之前依赖副作用代码 |
CJS 的 this 指向 module.exports | CJS 模块顶层 this !== globalThis | ESM 中顶层 this 是 undefined,不要依赖顶层 this |
exports 和 module.exports 混用 | 直接赋值 module.exports 后 exports 引用失效 | 只用一种方式导出,推荐 module.exports |
最佳实践
- 新项目一律用 ESM
- npm 包同时提供 ESM 和 CJS 入口(
exports字段) - 用
import()做动态导入和代码分割 - 浏览器中
<script type="module">默认 defer - 在
package.json中设置"sideEffects": false以启用 Tree Shaking - 避免使用
import * as命名空间导入,防止 Tree Shaking 失效 - ESM 中用
import.meta.url替代__dirname/__filename - 顶层 await 只用于必要的初始化场景(数据库连接、配置加载),避免滥用
- Dual Package 场景下,确保 ESM 入口 re-export CJS 入口,避免单例问题
面试题
Q1: CommonJS 和 ES Module 有什么区别?
核心区别:1) CJS 是运行时加载(动态),ESM 是编译时确定依赖(静态);2) CJS 导出的是值拷贝,ESM 导出的是引用(live binding);3) ESM 支持 Tree Shaking,CJS 不支持;4) CJS 的
require是同步的,ESM 的import是异步的;5) CJS 可动态条件导入,ESM 静态import必须在顶层(动态用import());6) CJS 的this指向module.exports,ESM 顶层this是undefined。
Q2: Tree Shaking 的原理是什么?为什么 CJS 不支持?
Tree Shaking 依赖 ES Module 的静态分析能力。打包工具在编译阶段分析
import/export,标记未使用的导出并在生成阶段移除。CJS 不支持因为其require是动态的(可在条件语句、循环中调用),打包工具无法在编译时确定哪些导出被使用,只能保守保留全部代码。
Q3: 模块循环依赖如何处理?CJS 和 ESM 行为有何不同?
CJS 循环依赖时,如果模块 A 还没执行完就被 B
require,B 拿到的 A 的导出是已执行部分,未执行部分为undefined(值拷贝)。ESM 循环依赖时,拿到的是引用绑定(live binding),即使模块未执行完,之后也能获取到最新值。建议:重构避免循环依赖,或提取共享逻辑到第三个模块。
Q4: UMD 是什么?解决了什么问题?
UMD(Universal Module Definition)是一种通用模块定义模式,通过运行时环境检测,让同一份代码同时兼容 CommonJS、AMD 和浏览器全局变量三种环境。它解决的是早期 JS 模块规范不统一的问题:一个库发布后需要同时支持 Node.js(CJS)、浏览器(全局变量/AMD),UMD 通过
typeof define/typeof module检测自动选择正确的导出方式。随着 ESM 的普及,UMD 已不再推荐,新项目应使用 ESM +package.json的exports条件导出。
Q5: 为什么 Vite 比 Webpack 快?和 ESM 有什么关系?
Vite 在开发模式下利用浏览器原生 ESM 实现”无打包”开发服务器:1) 浏览器直接通过
<script type="module">加载源码,Vite 只需按需编译当前请求的模块;2) 依赖预构建用 esbuild(Go 编写,比 JS 编写的 Webpack 快 10-100 倍);3) 不像 Webpack 那样启动时就要打包所有模块。ESM 的静态分析能力让 Vite 能精确知道模块依赖关系,实现按需编译和 HMR 精确更新。Webpack 5 也支持了模块联邦和缓存优化,但架构上仍是全量打包。
Q6: import() 动态导入和 require() 有什么区别?
import()返回 Promise,是异步加载;require()是同步加载;2)import()可在 ESM 和 CJS 中使用,require()只能在 CJS 中使用;3)import()支持 Tree Shaking(配合/* webpackChunkName */魔法注释),require()不支持;4)import()路径可以是变量,但无法静态分析;require()路径也可以是变量;5)import()主要用于代码分割和按需加载,require()用于 Node.js 同步加载模块;6)import()加载的是 ESM 模块,require()加载的是 CJS 模块。
Q7: 什么是 “barrel file”?对 Tree Shaking 有什么影响?
Barrel file(桶文件)是一种将多个模块的导出集中重新导出的文件,通常命名为
index.js,如export { A } from './a'; export { B } from './b';。它对 Tree Shaking 的影响:1) 某些打包器(早期 Webpack、Rollup 特定配置下)可能无法正确追踪 barrel file 的导出使用情况,导致整个文件被保留;2) 如果 barrel file 中有带副作用的模块(如export './polyfill'),即使只用了一个导出,其他模块也不会被移除;3) 解决方案:设置"sideEffects": false、使用深度导入路径(import { A } from './utils/a'而非import { A } from './utils')、避免在 barrel file 中导入有副作用的模块。
Q8: 顶层 await 有什么用?有什么限制?
顶层 await 允许在 ESM 模块顶层直接使用
await,无需包裹 async 函数。用途:1) 异步初始化(数据库连接、配置加载);2) 动态依赖解析;3) 资源获取后再导出(export const config = await loadConfig())。限制:1) 只能在 ESM 模块中使用("type": "module"或.mjs);2) 使用顶层 await 的模块变成异步模块,导入它的所有模块也会被隐式变为异步;3) 可能形成瀑布式加载(A 等待 → B 等待 → C),导致页面加载变慢;4) 不应在频繁被导入的基础模块中使用;5) 并行请求应使用Promise.all而非多个串行顶层 await。
相关链接:
- [[ES6+核心特性]]
- Webpack与Vite
- Promise与异步
- 设计模式
- TypeScript核心