什么是闭包《基础回顾篇》
在 JavaScript 中,每当函数被创建,它就捕获了当时可访问的全部变量,形成一条作用域链。当这个函数被传递到外部并继续执行时,这条作用域链依然有效——这种“函数 + 被捕获的变量”的组合,就是闭包。它既是实现私有状态的简洁手段,也是内存泄漏的常见源头。
垃圾回收机制
首先我们需要有一个概念,垃圾回收机制。JS很方便的通过自动内存管理实现了内存分配和闲置资源回收,无需开发者自行管理。但是,对于开发者来说,需要知道什么时候会清除垃圾,什么时候不会被清除。JS通过标记清理的方式,对所有使用的标量都打上标签,那如果没有标记的变量就会被清除。但是闭包作为一个特殊的存在,只要这个函数还能被外界访问,里面的变量就不会被清除,随着闭包的滥用,可能导致内存泄漏。而必须等到所有地方都用不到这个函数的时候,里面的变量才会被删除。
理解闭包
先说结论,非特殊情况,尽量避免产生闭包。
当内部函数能被外部函数访问的时候,就形成了一个闭包。
最常见的是
function A ()=>{
const string = 'hello'
return ()=>{
console.log(string)
}
}
A()() //hello
函数中返回了一个函数,那么外层函数作用域下的所有变量就不会被清掉。
当然这只是一种方式,还有像在setTimeout中,或是函数被push到数组中,都属于闭包。
对于闭包来说只是一个概念,并且并不具象,
正确的使用闭包
尽管闭包容易导致内存泄漏,隐式共享,调试困难等问题,但是如果正确使用,能处理很多复杂问题。比如在react中,useState等hooks,为什么能够返回的函数或值,其实就是将状态放到了闭包当中,就算函数重新创建,但是值并不会出现变化。
所以在正常的开发过程中,要注意
function getA ()=>{
const string = 'hello'
return ()=>{
console.log(string)
}
}
const a = getA()
a()
a= null
当使用完a之后,清除a,则闭包就不存在,也不会导致内存泄露。
// 绑
element.addEventListener('click', handleClick);
// 解绑
element.removeEventListener('click', handleClick);
当事件触发后,同样需要解绑事件,不会导致内存泄漏。
以上是避免,再说一些使用技巧
函数工厂
假设我们需要做一个不同产品,不同折扣的计算器。
// 函数工厂:输入折扣率,输出一个“专属打折函数”
function createDiscount(discount) {
// discount 被闭包捕获,成为每个返回函数的私有变量
return function (price) {
return price * discount; // 始终用当时捕获的折扣
};
}
/* --------- 使用 --------- */
const vip80 = createDiscount(0.8); // 8 折
const sale50 = createDiscount(0.5); // 5 折
console.log(vip80(100)); // 80
console.log(sale50(100)); // 50
// 再调用 vip80,它仍记得自己永远是 0.8
console.log(vip80(200)); // 160
更高级的使用闭包——柯里化
如果我想传递多个参数,但又想多次传递,最后一次得到结果。那么可以使用函数柯里化。假设,本来一次要付 3 笔钱,现在变成先付第 1 笔,再付第 2 笔,最后付第 3 笔;每一笔钱都被“收据”记下来,等最后一笔钱到账时一起结算。
// 普通函数:一次给 3 个参数
const add = (a, b, c) => a + b + c;
// 柯里化版本:分 3 次给
const curriedAdd = a => b => c => a + b + c;
curriedAdd(1)(2)(3); // 6
结合闭包
const add1 = curriedAdd(1); // 先付第 1 笔
add1(2)(3); // 6
add1(4)(5); // 10 1 被复用
总结
柯里化 = “分批传参 + 闭包记账”,让你用更细粒度、可复用的方式拼装函数逻辑。
现在应该就能理解闭包的实际价值。但是还是注意,如果没有典型应用场景,少用闭包。