DrillLab
第 33 / 105 道33 / 105 · #298

什么是闭包

What is a closure

先自己答,再往下看Answer it yourself first

一句话:函数记住了它定义时所在的作用域—— 即使外层函数已经执行完了, 里面的变量也还活着,因为这个函数还在引用它们。

为什么会这样?因为作用域链在定义时就绑好了(#297)。 外层函数返回后, 它的变量本该被回收, 但只要还有函数引用着, 垃圾回收就不会动它

四个真实用途(面试要的是用途,不是定义):

  • 私有状态—— 计数器、缓存。 外部拿不到那个变量,只能通过你暴露的方法改。
  • 防抖 / 节流—— 用闭包存 timer。
  • 柯里化 / 偏函数—— 记住已经传进来的参数(#299)。
  • React 的 Hooks 全靠它——useState 返回的setStateuseEffect 里的回调, 都是闭包捕获了那一次渲染的值。「过期闭包」这个 bug 就是它的副作用。

会追问的两道题:

循环里的 setTimeout(见 #282)。var 全程只有一个 i, 三个闭包共享它;let每次迭代新建一个绑定,所以各自记住自己那份。

闭包会不会造成内存泄漏?会 —— 如果闭包一直存活(比如挂在全局或未移除的事件监听里), 它引用的整个作用域都回收不了。解法是及时解绑, 这也是 React 里 useEffect必须写清理函数的原因之一。

In one line: a function remembers the scope it was defined in — even after the outer function has finished, the variables inside are still alive, because that function is still referencing them.

Why does that happen? Because the scope chain is wired up at definition time (#297). Once the outer function returns, its variables would normally be collected, but garbage collection leaves them alone as long as some function still references them.

Four real uses — the interview wants uses, not the definition:

  • Private state — counters, caches. Nothing outside can reach the variable; it can only go through the methods you expose.
  • Debounce and throttle — the closure holds the timer.
  • Currying and partial application — remembering the arguments received so far (#299).
  • Every React Hook rests on it — the setState returned by useState, and the callback inside useEffect, are closures that captured the values of one particular render. The “stale closure” bug is the flip side of that.

Two follow-ups to expect:

setTimeout inside a loop (see #282). With var there is one i for the whole loop and all three closures share it; let creates a new binding per iteration, so each closure remembers its own.

Can a closure leak memory? It can — if the closure stays alive (hanging off a global, or an event listener you never removed), the entire scope it references cannot be collected. The fix is to unsubscribe in time, which is one of the reasons a React useEffect needs its cleanup function.

JavaScript闭包的两道必考题Two closure questions that always come up示意Illustrative
1// 私有状态:外面拿不到 count,只能通过方法改
2function createCounter() {
3 let count = 0; // 外面访问不到
4 return {
5 inc: () => ++count,
6 get: () => count,
7 };
8}
9const c = createCounter();
10c.inc(); c.inc();
11c.get(); // 2
12c.count; // undefined ← 真的私有
13
14// 经典面试题:这里会打印什么?
15function f() {
16 const fns = [];
17 for (var i = 0; i < 3; i++) fns.push(() => i);
18 return fns.map((fn) => fn());
19}
20f(); // [3, 3, 3] —— 三个闭包共享同一个 i
21 // 把 var 换成 let 就是 [0, 1, 2]
1// Private state: the outside cannot reach count, only the methods change it
2function createCounter() {
3 let count = 0; // not reachable from outside
4 return {
5 inc: () => ++count,
6 get: () => count,
7 };
8}
9const c = createCounter();
10c.inc(); c.inc();
11c.get(); // 2
12c.count; // undefined ← genuinely private
13
14// A classic interview question: what does this print?
15function f() {
16 const fns = [];
17 for (var i = 0; i < 3; i++) fns.push(() => i);
18 return fns.map((fn) => fn());
19}
20f(); // [3, 3, 3] —— the three closures share one i
21 // change var to let and you get [0, 1, 2]