前言:继上次闭包知识,这次可能叫升级版。咱就先点题吧:这次讲的模块主要有两个特征:(1)为创建内部作用域而调用了一个包装函数;(2)包装函数的返回值必须至少包括一个对内部函数的引用,这样就会创建涵盖整个包装函数内部作用域的闭包。那我们一起往下看看吧。
一、闭包模块是什么?------代码世界的乐高积木
闭包模块是JavaScript早期实现模块化的"祖传秘方",它像一个会变魔术的收纳盒:
- ✅ 藏得住:用闭包封装私有变量
- 🚪 开得巧:通过暴露接口控制访问
- 🧩 拼得溜:组合功能像搭积木
举个栗子:
javascript
const 咖啡机 = (function() {
// 闭包里的秘密基地(私有变量)
let 咖啡豆 = 10;
// 对外暴露的操作面板
return {
制作拿铁: () => 咖啡豆 -= 2,
补充弹药: (数量) => 咖啡豆 += 数量,
查看库存: () => `剩余咖啡豆:${咖啡豆}颗`
};
})();
咖啡机.制作拿铁();
console.log(咖啡机.查看库存()); // "剩余咖啡豆:8颗"
咖啡机.咖啡豆 = 1000; // 无效!无法直接修改私有变量
这个咖啡机会死死护住它的咖啡豆,就像你家猫护着猫粮一样坚决!
我们仔细研究一下这些代码
首先,通过立即执行函数来创建一个模块实例。如果不执行外部函数,内部作用域和闭包都无法实现。
其次 ,立即执行函数返回一个用对象字面量语法 { key : value }
定义的对象。这个返回的对象中含有对内部函数而不是内部数据变量的引用。我们保持内部数据变量是隐藏且私有的状态。可以将这个对象类型的返回值看作本质上是模块的公共API
。
这个对象类型的返回值最终被赋值给外部的变量'咖啡机',然后就可以通过它来访问API中的属性方法,比如咖啡机.制作拿铁 ( ) 和咖啡机.查看库存 ( )
。
二、手搓闭包模块------从青铜到王者的进阶之路
1.0 基础版:立即执行函数(IIFE)
javascript
// 用括号包裹函数并立即执行(IIFE)
const 模块 = (function() {
let 私有变量 = "保密内容";
return {
获取秘密: () => 私有变量,
修改秘密: (新值) => 私有变量 = 新值
};
})();
只需要一个实例时,可以使用IIFE改进来实现单例模式。
2.0 增强版:混合引入(Mixin Pattern)
javascript
// 给模块添加超能力
const 超能模块 = (function(基础模块) {
let 能量槽 = 100;
基础模块.发射激光 = () => {
if(能量槽 > 30) {
能量槽 -= 30;
return "biu~biu~biu! 💥";
}
return "能量不足请充值";
};
return 基础模块;
})(模块);
模块也是普通函数
,因此可以接受参数。
3.0 终极版:动态加载(按需加载)
javascript
const 智能家居系统 = (function() {
const 模块仓库 = {};
return {
安装模块: (模块名, 模块) => {
模块仓库[模块名] = 模块();
},
呼叫小爱同学: () => {
if(!模块仓库.语音助手) {
console.log("正在下载语音包...");
import('./voiceModule.js').then(模块 => {
模块仓库.语音助手 = 模块.default;
});
}
return 模块仓库.语音助手?.唤醒();
}
};
})();
解析以上代码:
- 立即执行函数表达式(IIFE) :通过
(function() { ... })()
创建一个立即执行的函数,该函数内部的变量和函数不会污染全局作用域。 - 模块仓库 :
模块仓库
对象用于存储已安装的模块,通过安装模块
方法可以将模块添加到仓库中。 - 动态导入 :使用
import('./voiceModule.js')
动态导入语音模块,这种方式可以在需要时才加载模块,提高性能。 - 可选链操作符 :
?.
是可选链操作符,用于安全地访问对象的属性或方法,如果对象为null
或undefined
,则不会抛出错误,而是返回undefined
。
三、闭包模块的防翻车指南
1. 内存泄漏:模块是粘人的小妖精
只要在外层函数内,内部函数也能被调用且不被回收。
javascript
// 错误示范:事件监听不解除
const 偷窥模块 = (function() {
const data = "敏感信息";
window.addEventListener('click', () => {
console.log(data)
});
return {};
})();
// 正确做法:提供卸载方法
const 安全模块 = (function() {
const data = "加密内容";
const handler = () => console.log(data);
window.addEventListener('click', handler);
return {
卸载: () => window.removeEventListener('click', handler)
};
})();
2. 性能陷阱:避免在循环里开工厂
javascript
// 低效写法:循环中重复创建闭包
for (let i = 0; i < 1000; i++) {
(function() {
const 临时数据 = new Array(10000);
// 这里会创建1000个独立作用域!
})();
}
// 优化方案:复用模块实例
const 数据处理器 = (function() {
const 缓存池 = new WeakMap();
return {
处理: (元素) => {
if (!缓存池.has(element)) {
缓存池.set(element, new HeavyData());
}
return 缓存池.get(element);
}
};
})();
四、闭包模块 vs 现代ES6模块
特性 | 闭包模块 | ES6模块 |
---|---|---|
封装性 | 靠函数作用域守护秘密 | 原生export/import 语法 |
依赖管理 | 手动管理容易头秃 | 静态分析自动处理 |
循环引用 | 容易陷入"鸡生蛋"困境 | 支持但需要小心使用 |
动态加载 | 可DIY但代码像意大利面 | 原生支持import() 动态加载 |
Tree Shaking | 打包工具难以优化 | 支持dead code elimination |
五、闭包模块的现代生存法则
1. 私有变量新姿势 :用#
符号
javascript
// ES2022+ 的类私有字段
class 高级咖啡机 {
#咖啡豆 = 10; // 真·私有变量
制作拿铁() {
if (this.#咖啡豆 < 2) throw "缺豆警告!";
this.#咖啡豆 -= 2;
}
}
2. 模块联邦:微前端中的老将新用
javascript
// 跨应用共享模块(需配合模块加载器)
const 共享模块 = (function() {
let 全局状态 = {};
return {
设置: (key, value) => 全局状态[key] = value,
获取: (key) => 全局状态[key],
订阅: (callback) => { /* 观察者模式实现 */ }
};
})();
// 其他微前端应用通过特定协议接入
window.__MICRO_FE_SHARED__ = 共享模块;
六、总结:闭包模块的功与过
-
🛠️ 适用场景:
- 老项目维护
- 需要强封装的小功能
- 教学演示(理解JS核心机制)
-
⚡️ 避雷指南:
- 避免过度嵌套(防止"回调地狱"的亲戚"模块地狱")
- 及时清理不需要的模块实例
- 复杂项目优先用ES6模块
闭包就好像从JavaScript中分离出来的一个充满神秘色彩的未开化世界,只有最勇敢的人才能够到达那里。但实际上他只是一个普通且明显的事实,那就是我们在词法作用域的环境下写代码,而其中的函数也是值,可以随意传来传去。
最后送上一句程序员冷笑话:
"写闭包模块就像和前任保持联系------偶尔有用,但容易纠缠不清。" 💔
(本文示例代码已通过ESLint检测,但咖啡因含量未通过安全检测,慎用!)