一、从一个真实需求开始
在开发 3D 材质编辑器时,我遇到了一个性能问题:当用户拖动滑块调整材质的金属度(Metallic)时,会高频触发更新事件,导致预览图标产生很多不必要的渲染。
解决方案是节流(Throttle) ,而实现节流最优雅的方式就是闭包:
TypeScript
const MakeIconPreviewThrottled = (() => {
let lastTime = 0;
let timer = null;
const interval = 200;
return () => {
const now = Date.now();
const remaining = interval - (now - lastTime);
if (remaining <= 0) {
lastTime = now;
if (timer !== null) {
clearTimeout(timer);
timer = null;
}
MakeIconPreview(matManager.curMat);
} else if (timer === null) {
timer = window.setTimeout(() => {
lastTime = Date.now();
timer = null;
MakeIconPreview(matManager.curMat);
}, remaining);
}
};
})();
这段代码工作得很好,但当我向同事解释时,被问到一个直击灵魂的问题:"这里的 return 跳转到哪里了?"
二、IIFE:闭包的"舞台"
要理解 return 的去向,必须先理解 IIFE(Immediately Invoked Function Expression,立即执行函数表达式)。
2.1 两种函数定义的对比
TypeScript
// 方式一:函数声明(先定义,后执行)
function normalFunc() {
return "hello";
}
const result = normalFunc(); // 需要显式调用,return 回到 result
// 方式二:IIFE(定义的同时立即执行)
const result = (() => {
return "hello";
})(); // 末尾的 () 表示"立即执行",return 直接回到 result
2.2 执行流程图解

核心洞察 :IIFE 的 return 并不是"跳转"到某个遥远的地方,而是把值"交还"给表达式本身,然后整个 IIFE 表达式的结果就是那个返回值。
三、闭包的"魔法背包"
现在来看完整的闭包结构。当 IIFE 执行完毕后,它的局部变量应该被销毁,但闭包让它们"活"了下来。
3.1 变量生命周期图解

3.2 闭包的本质
闭包 = 函数 + 该函数被创建时所处的作用域环境
即使外部函数(IIFE)已经执行完毕,只要返回的函数还被引用,它就能一直访问那个"背包"里的变量。
四、return 的"跳跃"真相
现在回答核心问题:return 跳到哪里了?
4.1 代码层面的解释
TypeScript
const MakeIconPreviewThrottled = (() => {
// ... 一些变量定义
return () => { // ◄── 这个 return 去哪里?
// ... 节流逻辑
};
})(); // ◄── IIFE 立即调用,在这里"接住"返回值
答案 :return 把值交还给 IIFE 表达式本身,然后整个表达式的结果就是那个值,最终被赋给左边的变量。
4.2 类比理解
想象 IIFE 是一个自动售货机:

return 就是售货机的出货动作------它不把商品送到天涯海角,只是送到机器的取货口,然后由等号"接"住。
4.3 执行上下文视角
从 JavaScript 引擎的角度看:

return 的本质是:销毁当前执行上下文,并将值传递给调用者。
五、为什么需要 IIFE?不能直接用普通函数吗?
你可能会问:为什么要把代码包在 (() => { ... })() 里?不能直接写吗?
5.1 问题复现:没有闭包的"节流"
TypeScript
// ❌ 错误示范
function badThrottle() {
let lastTime = 0; // 每次调用都重新初始化!
let timer = null;
return function() {
// ... 节流逻辑
};
}
// 在观察者中
matManager.onSetCurMatMetallicObservable.add(() => {
badThrottle()(); // 每次事件都创建新的 lastTime!
// 或者
const fn = badThrottle(); // 还是新的 lastTime
fn();
});
问题 :每次调用 badThrottle() 都会创建全新的 lastTime 和 timer,节流完全失效!
5.2 正确 vs 错误的对比

5.3 IIFE 的作用总结
| 特性 | 说明 |
|---|---|
| 隔离作用域 | 避免 lastTime 等变量污染全局 |
| 立即执行 | 只执行一次,初始化状态 |
| 持久状态 | 返回的函数持续访问同一组变量 |
| 封装性 | 外部无法直接修改 lastTime,只能通过返回的函数操作 |
六、实际应用中的闭包模式
基于上面的理解,这里总结几种常见的闭包使用模式:
6.1 模块模式(Module Pattern)
TypeScript
const CounterModule = (() => {
let count = 0; // 私有变量
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
}
};
})();
CounterModule.increment(); // 1
CounterModule.increment(); // 2
CounterModule.getCount(); // 2
// count 无法从外部直接访问
6.2 函数工厂
TypeScript
const makeMultiplier = (factor) => {
return (number) => number * factor;
// 每个返回的函数都"记住"了自己的 factor
};
const double = makeMultiplier(2); // factor = 2
const triple = makeMultiplier(3); // factor = 3
double(5); // 10 (用的是自己的 factor=2)
triple(5); // 15 (用的是自己的 factor=3)
6.3 记忆化(Memoization)
TypeScript
const fibonacci = (() => {
const cache = {}; // 缓存闭包
const fib = (n) => {
if (n < 2) return n;
if (cache[n]) return cache[n];
cache[n] = fib(n - 1) + fib(n - 2);
return cache[n];
};
return fib;
})();
fibonacci(50); // 瞬间完成,因为有缓存
七、总结:闭包与 return 的核心认知
7.1 三个关键认知
-
IIFE 的
return是"出货":它把值交给表达式本身,不是跳转到某个标签或函数 -
闭包是"背包机制":函数背着定义时的变量环境到处走
-
状态持久化:IIFE 只执行一次,但返回的函数可以多次调用,共享同一组变量
7.2 回到最初的代码
TypeScript
const MakeIconPreviewThrottled = (() => {
let lastTime = 0; // ◄── 只初始化一次
let timer = null; // ◄── 只初始化一次
return () => { // ◄── 返回的函数"背"着上面两个变量
// 节流逻辑
};
})(); // ◄── 立即执行,return 的值赋给 MakeIconPreviewThrottled
// 之后每次调用:
MakeIconPreviewThrottled(); // 访问的是同一个 lastTime 和 timer
7.3 一张图记住所有

八、写在最后
闭包是 JavaScript 中最强大也最容易让人困惑的特性之一。理解它的关键在于:
-
不要纠结
return跳到哪里------它只是把值交给调用者 -
关注"背包"里装了什么------闭包保存的是变量引用,不是值的副本
-
IIFE 是创建私有作用域的利器 ------在现代 JS 中,虽然可以用
let/const块级作用域,但 IIFE 仍然是封装逻辑的经典模式