闭包(Closure):
内部函数可以访问外部函数作用域的变量,并且外部函数执行结束后,变量不会被销毁
快速判断是不是闭包:
满足两点就是闭包:
- 内层函数引用了外层函数的变量
- 内层函数在外层函数作用域之外执行
最简单的例子:
js
function outer() {
let msg = 'hello closure';
// 内部函数 inner
function inner() {
console.log(msg); // inner 能访问 outer 的变量
}
return inner; // 返回内部函数
}
const fn = outer();
fn(); // hello closure
执行过程:
outer()执行,创建变量msg、函数innerouter返回inner,outer 函数执行完毕,正常情况下函数内变量会销毁- 但
inner形成闭包,依然持有msg的引用 - 调用
fn(),还能读取到msg
使用场景(防抖、私有变量、Vue3 hooks)
面试话术: "闭包在实战里到处都是,最典型的有三个:"
- 防抖/节流(防抖为什么能记住 timer?) "我们写防抖的时候,外层函数里定义了一个 let timer = null,然后返回一个内层函数。因为闭包,内层函数每次执行都能访问到同一个 timer。所以第一次触发设了定时器,第二次触发才能 clearTimeout把它清掉。没有闭包,防抖根本写不出来。"
js
function debounce(fn, delay) {
let timer = null // 闭包变量
return function (...args) {
clearTimeout(timer) // 访问外层 timer
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
}
- 私有变量(怎么防止别人乱改数据?) "比如我想做一个计数器,我不希望别人直接 counter.count = 100 去改它。我就可以用闭包,把 count藏在外层函数里,只返回 increment、decrement 和 getCount 三个方法。这样外部只能通过我提供的方法来操作,数据就私有化了。"
js
function createCounter() {
// 1. 私有变量,外部访问不到
let count = 0
// 2. 返回三个方法,它们都是闭包
return {
increment() {
count++ // 3. 内部方法可以访问闭包里的 count
},
decrement() {
count--
},
getCount() {
return count // 4. 只提供读取方法,不提供直接修改的途径
}
}
}
const counter = createCounter()
counter.increment()
counter.increment()
console.log(counter.getCount()) // 2 原因:counter.increment() 执行两次
// 外部尝试直接修改,看看会发生什么?
counter.count = 100 // 只是给 counter 对象加了个新属性,根本没碰到里面的 count
console.log(counter.getCount()) // 依然是 2!(数据没被破坏)因为 getCount 只认闭包里的那个 count。
console.log(counter.count) // 100(但这个 100 是个假象,和闭包里的 count 毫无关系)
- Vue3 的组合式函数(useXXX 为什么能记住状态?) "我们写 Vue3 的 useCounter 或者 useRequest 时,里面写的 ref、reactive 变量,其实都处于闭包环境里。外层函数 useCounter() 执行完,按理说变量该销毁,但因为返回的方法(比如 increment)还在引用它们,所以这些状态一直活在内存里。Vue3 的 hooks 本质上就是闭包的集合。"
js
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
return { count, increment }
}
increment 能访问 count,因为它是闭包。
useCounter 执行完了,count 还在,因为 increment 还引用着它。
闭包缺点
- 内存泄漏风险:闭包会保留外层变量引用,变量不会被 GC 回收;大量使用容易占用内存。
- 滥用闭包会让代码可读性变差、调试困难。
内存泄漏问题解决
不用了要及时断开引用
- 如果闭包返回的函数不用了,就把它置空(比如
fn = null)。 - 如果是组件里用的闭包(比如防抖),在组件卸载时(
onUnmounted)一定要清掉定时器、解除事件监听。 - 不要无脑把大对象放在闭包外层,用不到就别放进去。
其实现在现代浏览器(V8)很聪明,它只会保留闭包里真正用到的变量,所以不用过度恐慌,但该清理的时候一定要清理。