JavaScript闭包到底怎么理解有哪些实际应用场景

juming
juming 正式会员超兽战士
发布于 2026-10-07 05:49 ·0 浏览 ·0 回复

结论:闭包就是「函数 + 函数定义时所在的那条作用域链」,只要一个函数在它被定义的作用域之外被调用、却依然能读到定义时的变量,它就用到了闭包。实际开发中最常见的落点是四类:私有变量与模块封装、防抖节流这类需要记住上一次状态的工具函数、柯里化与记忆化这类函数工厂、以及事件回调和 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 会做作用域分析,只保留真正被内层函数引用的变量,没被用到的不会被一起留住,所以不必为了“省内存”把闭包写得很别扭;第三,别在高频函数或循环里往闭包缓存无上限地塞数据。

面试被问到,一句话怎么答?

结论:闭包是函数和它定义时词法环境的组合,让函数在定义作用域之外执行时依然能访问那些变量;它带来私有变量、状态保持和函数工厂三种能力,代价是被捕获的变量常驻内存。

把这两句记住,再能现场写出一个计数器和一段防抖,闭包这道题基本就答满了。真正区分水平的不是定义,而是你能说清哪次调用让变量活了下来、什么时候该手动断开引用。

版权声明:本文来自 GJ论坛《JavaScript闭包到底怎么理解有哪些实际应用场景》
原文链接:https://www.gj0.com/thread-105.html
转载请注明出处并保留本声明;内容仅代表作者观点,与本站立场无关。若本文涉嫌侵权,请联系本站处理。

全部回复 0

还没有回复,来抢沙发~