**八股问法:**什么是闭包?有什么作用和缺点?在哪里会用到闭包
什么是闭包?
闭包的本质是函数加上其所处词法环境的组合。
闭包内层函数 + 引用的外层函数变量。也就是一个内部函数可以访问其外部函数作用域的变量,其实外部函数已经执行完毕并销毁,内部函数依然能持有对外部作用域变量的引用。
闭包 是由捆绑起来(封闭的)的函数和函数周围状态(词法环境 )的引用组合而成。换言之,闭包让函数能访问它的外部作用域。在 JavaScript 中,闭包会随着函数的创建而同时创建。
闭包的变量存在堆中,即解释了函数调用之后为什么闭包还能引用到函数内的变量。
通俗理解闭包:一个房间(外层函数)中有一个小孩(内层函数)还有零花钱(外层函数变量),小孩可以花零花钱(内层函数可以访问外层函数变量),也就是内层函数+可访问的外层变量(小孩+可以花的零花钱)就是闭包。
javascript
// 外层函数:独立房间
function outer() {
let money = 100; // 房间私有变量:零花钱
// 内层函数:小孩
function inner() {
console.log(money); // 小孩能用房间的钱
}
return inner; // 把小孩送出房间
}
// child 就是被送出去的内层函数
const child = outer();
// 跑到外面执行,依然能拿到 money
child(); // 输出:100

闭包的核心作用
1)封装私有变量、方法
解决全局变量污染问题,实现变量私有化,只有暴露出来的方法才能访问内部变量
// 实战:封装计数器,避免count暴露在全局
function createCounter() {
let count = 0; // 私有变量,外部无法直接访问
return {
increment: () => count++, // 暴露方法操作私有变量
getCount: () => count
}
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(window.count); // undefined(变量私有化)
2)延长 局部变量的生命周期
函数执行完之后,局部变量不被销毁,可以后续复用
场景:防抖、节流(保存定时器),ajax请求的回调中请求访问参数
3)实现柯里化:可以把多参数函数拆解为单参数函数,提高函数的复用性
三个特性
- ①函数嵌套函数
- ②函数内部可以引用函数外部的参数和变量
- ③参数和变量不会被垃圾回收机制回收
闭包的缺点及解决方案
闭包会长期持有对外部变量的引用,导致这些变量无法被垃圾回收,如果大量使用闭包或者闭包未及时释放,会占用过多的内存,甚至导致页面卡顿
**解决方法:**手动解除引用、避免闭包持有不必要的大对象、合理控制闭包的适用范围,避免全局范围长期持有闭包
闭包的形成需要的条件:
- 存在函数嵌套:外部函数+内部函数
- 内部函数引用了外部函数作用域的变量、方法
- 外部函数执行后,其返回值(内部函数)被外部变量所持有(或者是内部函数以其他形式被外部访问)
内存泄漏
闭包不一定都有return和内存泄漏
有return:如果外部函数想要使用闭包中的函数
每一个函数都会拷贝上级作用域,形成一个作用域链条。
哪里会用到闭包?
第一个最常用的就是防抖和节流。比如搜索框输入、窗口 resize、滚动监听,我们会把定时器 ID 存在外层函数里,返回的真正执行函数通过闭包一直持有这个 timer 变量,这样每次触发才能清楚上一次的定时器,实现防抖效果。如果没有闭包,timer 每次都会被回收,就做不到连续控制。
javascript
function debounce(fn, delay) {
let timer = null; // 闭包变量
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
function throttle(fn, delay) {
let timer = null;
return function(...args) {
if (timer) return;
timer = setTimeout(() => {
fn.apply(this, args);
timer = null;
}, delay);
};
}
第二个是实现私有变量或模块化。比如封装一个计数器,把 count 放在外部函数作用域里,只暴露 increment、getCount 这些方法,外部没办法直接修改 count,只能通过提供的方法操作,这就是用闭包做数据私有化,避免全局变量污染,也是早期模块化常用的方式。
javascript
function createCounter() {
let count = 0; // 私有变量,外部无法访问
return {
increment: () => count++,
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
第三个是异步场景保存现场,比如循环里绑定事件、定时器。像 for 循环注册多个点击事件,如果不用 let 或闭包,所有回调都会共享同一个变量,导致结果不符合预期。通过闭包把每次循环的 i 保存下来,每个回调就能拿到正确的值,这也是非常高频的用法。
javascript
for(var i = 0; i < 5; i++) {
(function (i) {
setTimeout(() => console.log(i), 1000);
})(i);
}
第四个是函数柯里化和参数复用。比如封装一个通用请求函数,先传入 baseURL 固定下来,返回的新函数再接收 url 和 params,后面调用时就不用重复传域名,这里也是靠闭包记住外层的 baseURL,实现参数预置。
javascript
function requestWithBaseUrl(baseURL) {
// 闭包记住 baseURL
return function (url, params) {
return fetch(baseURL + url, { params });
};
}
const api = requestWithBaseUrl('https://api.xxx.com');
api('/user'); // 自动带上 baseURL
// 基础柯里化:拆分参数、分步接收
function add(a) {
return function (b) {
return function (c) {
return a + b + c;
}
}
}
// 调用
add(1)(2)(3); // 6
还有像自定义 hooks 里保存状态 、事件监听里缓存 DOM 或配置 ,本质上也都是闭包在起作用。简单说,只要是需要长期保存一些变量,又不想暴露到全局的场景,基本都在用闭包。