JavaScript闭包到底怎么理解有哪些实际应用场景
结论:闭包就是「函数 + 函数定义时所在的那条作用域链」,只要一个函数在它被定义的作用域之外被调用、却依然能读到定义时的变量,它就用到了闭包。实际开发中最常见的落点是四类:私有变量与模块封装、防抖节流这类需要记住上一次状态的工具函数、柯里化与记忆化这类函数工厂、以及事件回调和 React Hooks 里的状态保存。
闭包到底是怎么形成的?
结论:闭包的形成只需要三个条件同时成立——存在内层函数、内层函数引用了外层函数的变量、内层函数在外层函数执行完之后仍然被调用。
function outer() {
let count = 0; // 外层局部变量
return function inner() { // 内层函数引用了 count
return ++count;
};
}
const inc = outer(); // outer 已经执行完
inc(); // 1
inc(); // 2
按常规理解,outer 执行完它的局部变量就该销毁。但 inner 的作用域链上挂着 outer 的变量对象,V8 判定 count 仍被引用,于是不回收。所以「闭包 = 函数 + 它记住的环境」这句话,重点在“记住”两个字上。
注意:不一定非要 return 一个函数。把回调传给 setTimeout、绑到 addEventListener 上,同样构成闭包。
循环里的 var 为什么打印出 3 3 3?
结论:var 是函数作用域,三次循环共用同一个 i,而 setTimeout 回调执行时循环早已结束,读到的都是同一个终值 3。
for (var i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 3 3 3
for (let i = 0; i < 3; i++) setTimeout(() => console.log(i)); // 0 1 2
let 在每次迭代都会创建一个新的绑定,等于每轮循环生成了独立的闭包环境。ES5 时代的解法是套一层 IIFE,手动为每轮循环造一个作用域。
实际场景一:私有变量与模块封装
结论:用 IIFE 或闭包把变量关在函数作用域里,只对外暴露读写接口,是 ES Module 之前最主流的“私有成员”方案,今天依然可用。
const counter = (() => {
let count = 0; // 外部拿不到
return {
inc: () => ++count,
get: () => count,
};
})();
counter.inc();
counter.get(); // 1
好处是 count 无法从外部直接赋值,避免被随意篡改。现在也可以用 class 的 #private 字段实现同样的效果,但闭包方案不依赖 class 语法,兼容性更好。
实际场景二:防抖和节流
结论:防抖、节流必须“记住上一次触发的时间或定时器”,这个状态只能靠闭包保存,否则接口就要挂到全局变量或实例属性上。
function debounce(fn, delay = 300) {
let timer = null; // 闭包变量
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
常见参数:搜索框输入用 300ms 防抖,窗口 resize 用 200ms,滚动加载用 100ms 节流(时间戳差值判断)。这几个数字都是经验值,按业务调,但原理相同——状态活在闭包里。
实际场景三:函数工厂、柯里化与记忆化
结论:凡是要“带着配置或缓存生成一个新函数”的地方,实现方式基本都是闭包。
const add = a => b => a + b; // 柯里化
const add10 = add(10); // a = 10 被记住
add10(5); // 15
function memoize(fn) {
const cache = new Map(); // 闭包缓存
return n => {
if (cache.has(n)) return cache.get(n);
const v = fn(n);
cache.set(n, v);
return v;
};
}
记忆化用在朴素递归求斐波那契上,能把时间复杂度从 O(2^n) 降到 O(n)。代价是 cache 会一直活着,属于典型的空间换时间,缓存数量需要设上限。
实际场景四:回调里保存状态
结论:事件回调和 React 的 useState/useEffect 都依赖闭包,把“那一次执行时的值”固定下来。
const btns = document.querySelectorAll('button');
for (let i = 0; i < btns.length; i++) {
btns[i].onclick = () => console.log(i); // 0 1 2
}
React 里著名的“闭包陷阱”也是同一回事:useEffect 里读到的 state 是那一次渲染的快照,不是最新值。要拿最新值,可以用 useRef 保存,或者把它写进依赖数组。
闭包会导致内存泄漏吗?
结论:闭包本身不会造成泄漏,但它会让被引用的变量活到闭包自己被回收为止;只要闭包还挂在全局、DOM 事件或定时器上,它捕获的变量就不会被 GC 回收。
三个实操注意点:第一,不再需要时显式断开引用,比如 el.onclick = null、clearInterval(timer);第二,现代 V8 会做作用域分析,只保留真正被内层函数引用的变量,没被用到的不会被一起留住,所以不必为了“省内存”把闭包写得很别扭;第三,别在高频函数或循环里往闭包缓存无上限地塞数据。
面试被问到,一句话怎么答?
结论:闭包是函数和它定义时词法环境的组合,让函数在定义作用域之外执行时依然能访问那些变量;它带来私有变量、状态保持和函数工厂三种能力,代价是被捕获的变量常驻内存。
把这两句记住,再能现场写出一个计数器和一段防抖,闭包这道题基本就答满了。真正区分水平的不是定义,而是你能说清哪次调用让变量活了下来、什么时候该手动断开引用。
原文链接:https://www.gj0.com/thread-105.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。