引言
在前端开发中,模块化是必不可少的重要概念。随着 JavaScript 生态的发展,CommonJS 和 ES Module 成为最主流的两种模块化方案。本文将深入剖析两者的核心原理,并通过典型面试题帮助大家彻底掌握模块化知识。
一、CommonJS 模块原理深度解析
1. require 函数执行机制
动态依赖特性
- 灵活的位置要求:require 可以出现在代码的任何位置(条件判断/循环/函数调用中)
- 运行时确定依赖:必须运行代码后才能确定依赖关系
- 同步执行机制:会阻塞后续代码直到模块加载完成
执行流程详解
javascript
// 伪代码展示 require 核心逻辑
function require(modulePath) {
// 1. 缓存检查
if (cache[modulePath]) {
return cache[modulePath].exports;
}
// 2. 创建模块对象
const module = {
exports: {},
id: modulePath,
loaded: false
};
// 3. 执行模块代码
const wrapperFunction = wrapModule(modulePath);
wrapperFunction.call(
module.exports,
module.exports,
require,
module,
__filename,
__dirname
);
// 4. 更新缓存
cache[modulePath] = module;
return module.exports;
}
2. 模块缓存机制
缓存验证策略
- 每个模块以唯一文件路径作为 ID 标识
- 通过模块 ID 检查是否已有缓存
缓存处理流程
javascript
// 缓存命中示例
const moduleA = require('./a'); // 首次加载,执行完整流程
const moduleA2 = require('./a'); // 命中缓存,直接返回结果
console.log(moduleA === moduleA2); // true
3. 模块作用域隔离原理
_run 函数的关键作用
javascript
// 模块实际执行环境
function _run(exports, require, module, __filename, __dirname) {
// 你的模块代码在这里执行
console.log(arguments.length); // 5个参数
}
// 证明方法:在模块中输出 arguments
console.log(arguments); // 显示函数环境特征
参数来源说明
exports
和module.exports
:初始均为空对象{}
__filename
和__dirname
:Node.js 自动注入的路径信息
4. 导出机制核心要点
exports 与 module.exports 的关系
javascript
// 初始状态:指向同一对象
console.log(exports === module.exports); // true
// 正确用法:添加属性
exports.a = 1;
module.exports.b = 2;
// 此时导出:{ a: 1, b: 2 }
// 错误用法:直接赋值
exports = { a: 1 }; // 切断引用关系!
module.exports = { b: 2 }; // 正确,但会覆盖之前操作
经典面试题分析
javascript
// 案例1:基础绑定
this.a = 1;
exports.b = 2;
// 结果:导出 {a:1, b:2}
// 案例2:重新赋值
exports.a = 1;
module.exports = {b:2};
// 结果:仅导出 {b:2}
// 案例3:复合操作
exports.a = 'a';
module.exports.b = 'b';
this.c = 'c';
module.exports = {d:'d'};
// 结果:仅导出 {d:'d'}
二、ES Module 核心原理
1. 基本特性
- 官方标准:ES2015(ES6) 正式发布的语言标准
- 语法级支持 :使用
import/export
关键字 - 全环境兼容:现代浏览器和 Node.js 均支持
- 依赖类型:支持静态和动态两种依赖方式
2. 静态依赖特点
严格的语法要求
javascript
// ✅ 正确写法 - 模块顶部
import a from './a.js';
import { count } from './counter.js';
// ❌ 错误写法 - 不能在条件判断中
if (condition) {
import a from './a.js'; // SyntaxError
}
浏览器环境配置
html
<!-- 必须设置 type="module" -->
<script type="module">
import { count } from './counter.js';
</script>
3. 动态依赖机制
异步加载特性
javascript
// 动态 import() 返回 Promise
if (route === '/home') {
import('./views/Home.vue')
.then(module => {
// 默认导出在 module.default
const Component = module.default;
});
}
// 配合 async/await
async function loadComponent(route) {
const module = await import(`./views/${route}.vue`);
return module.default;
}
4. 符号绑定(实时绑定)
内存共享机制
javascript
// counter.js
export let count = 1;
export function increase() { count++; }
// main.js
import { count, increase } from './counter.js';
console.log(count); // 1
increase();
console.log(count); // 2 - 实时更新!
绑定关系解析
javascript
// 解构赋值会创建新变量,打破绑定
import { count as c, increase } from './counter.js';
console.log(c); // 1
increase();
console.log(c); // 1 - 值拷贝,不再绑定
三、CommonJS vs ES Module 核心差异
1. 标准与实现对比
特性 | CommonJS | ES Module |
---|---|---|
标准类型 | 社区标准 | 官方标准 |
实现方式 | API 函数 | 语法关键字 |
环境支持 | 仅 Node.js | 全环境支持 |
执行特性 | 动态依赖,同步执行 | 静态+动态,异步执行 |
2. 导出机制差异
具名导出 vs 默认导出
javascript
// ES Module - 具名导出
export const name = 'Alice';
export function hello() { }
// ES Module - 默认导出
export default class User { }
// CommonJS - 多种导出方式
module.exports = { name: 'Alice' };
exports.hello = function() { };
3. 值传递 vs 符号绑定
javascript
// CommonJS - 值拷贝
// counter.js
let count = 1;
module.exports = { count, increase: () => count++ };
// main.js
const { count, increase } = require('./counter');
increase();
console.log(count); // 1 - 值不变
// ES Module - 实时绑定
// main.js
import { count, increase } from './counter';
increase();
console.log(count); // 2 - 实时更新
四、高频面试题精讲
面试题1:CommonJS 和 ES6 模块的区别是什么?
参考答案:
- 标准差异:CommonJS 是社区标准,ES Module 是官方标准
- 实现方式:CommonJS 通过 API 函数实现,ES Module 通过语法关键字实现
- 环境支持:CommonJS 主要在 Node.js 环境,ES Module 全环境支持
- 执行机制:CommonJS 动态依赖同步执行,ES Module 支持静态分析且动态依赖异步执行
- 绑定机制:ES Module 具有符号绑定(实时绑定),CommonJS 是值拷贝
面试题2:export 和 export default 的区别是什么?
参考答案:
-
export(具名导出) :
- 必须带有命名(变量、函数定义等)
- 在模块对象中,命名即为属性名
- 一个模块可以有多个具名导出
-
export default(默认导出) :
- 在模块对象中固定为 default 属性
- 通常导出表达式或字面量,无需命名
- 一个模块只能有一个默认导出
面试题3:符号绑定的实际应用场景
实战案例:
javascript
// 状态管理场景
// store.js
export let state = { count: 0 };
export function setState(newState) {
state = { ...state, ...newState };
}
// component.js
import { state, setState } from './store.js';
// 所有导入模块都能获取到最新的状态
setState({ count: 1 });
console.log(state.count); // 1 - 实时更新