本回答由Deepseek AI 生成,内容仅供参考,请仔细甄别。
JavaScript 模块单例模式详解
在 JavaScript 模块系统中,单例 意味着同一个模块在整个应用程序中只会被加载和执行一次,无论它被导入多少次。
核心特性对比表格
| 方面 | 传统脚本 | ES6 模块 |
|---|---|---|
| 实例化次数 | 每次引入都重新执行 | 只执行一次 |
| 状态共享 | 每个 script 标签独立状态 | 全局共享状态 |
| 内存使用 | 可能重复占用内存 | 内存高效共享 |
| 副作用执行 | 每次引入都执行副作用 | 副作用只执行一次 |
模块缓存机制表格
| 缓存级别 | 描述 | 示例 |
|---|---|---|
| 模块标识符 | 通过完整路径标识模块 | './utils.js' ≠ './utils' |
| 执行状态 | 记录模块是否已执行 | executed: true/false |
| 导出对象 | 缓存最终的导出对象 | export { a, b, c } |
| 依赖关系 | 维护模块依赖图 | 避免循环依赖死锁 |
实际应用场景
javascript
// 1. 全局状态管理
// store.js - 全局状态单例
let state = {
user: null,
theme: 'light',
preferences: {}
};
export const getState = () => state;
export const setState = (newState) => {
state = { ...state, ...newState };
};
// 在任何文件中导入都是同一个 store
// 2. 服务层单例
// api-service.js
class ApiService {
constructor() {
this.baseURL = 'https://api.example.com';
this.cache = new Map();
}
async get(url) {
if (this.cache.has(url)) {
return this.cache.get(url);
}
const response = await fetch(`${this.baseURL}${url}`);
const data = await response.json();
this.cache.set(url, data);
return data;
}
}
// 导出单例实例
export default new ApiService();
// 3. 配置管理
// config.js
const config = {
apiKey: process.env.API_KEY,
environment: process.env.NODE_ENV,
debug: true
};
// Object.freeze 确保配置不被修改
export default Object.freeze(config);
与普通类的对比表格
| 方面 | 模块单例 | 类实例 |
|---|---|---|
| 创建方式 | export default new MyClass() |
new MyClass() |
| 实例数量 | 全局唯一 | 可创建多个实例 |
| 状态共享 | 自动共享 | 需要手动实现单例模式 |
| 内存占用 | 固定 | 每个实例独立占用 |
| 使用方式 | import singleton from './module' |
const instance = new MyClass() |
循环依赖中的单例行为
javascript
// moduleA.js
import { bFunction } from './moduleB.js';
console.log('模块A初始化');
export const aFunction = () => {
console.log('A函数调用');
bFunction();
};
// moduleB.js
import { aFunction } from './moduleA.js';
console.log('模块B初始化');
export const bFunction = () => {
console.log('B函数调用');
};
---
## 单例的优缺点表格
### 优点
| 优点 | 描述 | 示例 |
|------|------|------|
| **状态一致性** | 全局状态统一 | 用户登录状态 |
| **资源节约** | 避免重复创建 | 数据库连接池 |
| **配置统一** | 配置信息一致 | 应用设置 |
| **内存高效** | 减少内存占用 | 缓存管理 |
### 缺点
| 缺点 | 描述 | 解决方案 |
|------|------|----------|
| **测试困难** | 状态共享影响测试 | 依赖注入 |
| **耦合度高** | 模块间隐式依赖 | 明确接口定义 |
| **内存泄漏** | 单例常驻内存 | 合理设计生命周期 |
| **并发问题** | 多线程环境竞态条件 | 线程安全设计 |
---
## 测试中的单例处理
### 问题代码
```javascript
// user-service.js
class UserService {
constructor() {
this.users = new Map();
}
addUser(user) {
this.users.set(user.id, user);
}
}
export default new UserService(); // 单例导出
// test.js
import userService from './user-service.js';
test('添加用户', () => {
userService.addUser({ id: 1, name: 'Alice' });
// 这个测试会影响其他测试!
});
// 解决方案
// 方案1: 导出类而不是实例
export class UserService {
constructor() {
this.users = new Map();
}
}
// 方案2: 提供重置方法
class UserService {
constructor() {
this.reset();
}
reset() {
this.users = new Map();
}
}
export default new UserService();
总结
JavaScript 模块单例的核心要点:
-
✅ 一次性执行 - 模块代码只执行一次
-
✅ 状态共享 - 所有导入共享同一状态
-
✅ 缓存机制 - 模块系统自动缓存导出
-
✅ 路径标识 - 相同路径 = 相同模块实例
-
⚠️ 测试注意 - 需要处理测试中的状态污染
适用场景:
-
全局状态管理(Redux store)
-
服务层实例(HTTP client)
-
配置信息
-
工具类实例
-
缓存管理
避免场景:
-
需要多实例的类
-
测试敏感的代码
-
需要动态配置的对象
编程中的 "副作用" 详解
关联文章阅读推荐
JavaScript 传统脚本 vs 模块脚本(附:Tree-shaking、Polyfill 详解)
副作用 是指函数或表达式除了返回一个值之外,还对程序状态或外部环境产生了可观察的影响。
核心概念对比表格
| 方面 | 无副作用(纯函数) | 有副作用 |
|---|---|---|
| 定义 | 只依赖输入,只返回输出 | 修改外部状态或与环境交互 |
| 可预测性 | 高(相同输入总是相同输出) | 低(结果可能受外部影响) |
| 测试难度 | 容易 | 困难 |
| 引用透明 | 是 | 否 |
副作用的具体类型
1. 修改外部变量
// 有副作用 - 修改外部状态
let count = 0;
function increment() {
count++; // 副作用:修改外部变量
return count;
}
// 无副作用版本
function pureIncrement(num) {
return num + 1; // 不修改外部状态
}
2. 修改参数对象
// 有副作用 - 修改输入参数
function updateUser(user) {
user.lastLogin = new Date(); // 副作用:修改传入的对象
return user;
}
// 无副作用版本
function pureUpdateUser(user) {
return {
...user, // 创建新对象
lastLogin: new Date() // 不修改原对象
};
}
3. I/O 操作
// 有副作用 - I/O 操作
function saveToFile(data) {
fs.writeFile('data.json', data); // 副作用:文件系统操作
console.log('Saved!'); // 副作用:控制台输出
}
function fetchData() {
return fetch('/api/data'); // 副作用:网络请求
}
4. DOM 操作
// 有副作用 - DOM 修改
function renderMessage(message) {
document.getElementById('message').textContent = message; // 副作用:修改DOM
}
function addClickListener() {
button.addEventListener('click', handler); // 副作用:事件监听
}
模块中的副作用表格
| 副作用类型 | 模块中的示例 | 执行次数 |
|---|---|---|
| 全局变量修改 | window.globalVar = value |
只执行一次 |
| 原型扩展 | Array.prototype.newMethod = ... |
只执行一次 |
| 定时器 | setInterval(() => {}, 1000) |
只执行一次 |
| 事件监听 | document.addEventListener(...) |
只执行一次 |
| CSS 注入 | document.styleSheets[0].insertRule(...) |
只执行一次 |
副作用在模块系统中的重要性
模块缓存的影响
// config-module.js
console.log('初始化配置模块...'); // 副作用
// 模拟耗时的初始化
const config = (() => {
console.log('执行复杂配置逻辑...'); // 副作用
return { apiUrl: 'https://api.example.com' };
})();
export default config;
多次导入行为
| 导入次数 | 控制台输出 | 配置逻辑执行 |
|---|---|---|
| 第一次 | ✅ "初始化配置模块..." ✅ "执行复杂配置逻辑..." | ✅ 执行 |
| 第二次 | ❌ 无输出 | ❌ 不执行 |
| 第三次 | ❌ 无输出 | ❌ 不执行 |
副作用分类表格
明显的副作用
| 类型 | 示例 | 影响 |
|---|---|---|
| 全局状态修改 | window.obj = {} |
影响整个应用 |
| DOM 操作 | element.innerHTML = '' |
影响页面渲染 |
| 网络请求 | fetch('/api') |
影响服务器 |
| 文件操作 | fs.writeFile() |
影响文件系统 |
不明显的副作用
| 类型 | 示例 | 影响 |
|---|---|---|
| 随机数生成 | Math.random() |
每次结果不同 |
| 日期时间 | new Date() |
随时间变化 |
| 控制台输出 | console.log() |
开发工具输出 |
| 性能测量 | performance.now() |
测量结果变化 |
副作用的管理
1. 隔离副作用
// 将副作用集中管理
// side-effects.js
export const initializeApp = () => {
console.log('应用初始化');
window.appConfig = { /* ... */ };
document.title = '我的应用';
};
// 纯逻辑模块
// utils.js
export const calculate = (a, b) => a + b; // 无副作用
2. 明确标记
// 使用注释标记副作用
function processData(data) {
// 副作用:修改输入参数
data.processed = true;
/*#__PURE__*/
const result = data.value * 2; // 纯计算部分
return result;
}
3. package.json 声明
{
"sideEffects": [
"**/*.css",
"**/*.scss",
"src/polyfills.js",
"src/initialize.js"
]
}
实际开发中的副作用场景
Node.js 模块中的副作用
// database-connection.js
let connection = null;
// 副作用:数据库连接
export const getConnection = () => {
if (!connection) {
console.log('创建数据库连接...'); // 副作用
connection = mysql.createConnection(/* config */);
connection.connect(); // 副作用:建立连接
}
return connection;
};
总结
副作用的本质:
-
🔄 状态改变 - 修改了程序外部的状态
-
🌍 环境影响 - 与外部世界进行了交互
-
📝 可观察 - 产生了除返回值之外的影响
在模块系统中的重要性:
-
✅ 单例保证 - 副作用只执行一次
-
🚫 Tree-shaking - 有副作用的代码无法被移除
-
🔧 可预测性 - 知道副作用何时发生
开发建议:
-
最小化副作用,集中管理
-
明确标记有副作用的代码
-
在模块边界处理副作用
-
保持核心逻辑的纯净性