闭包的定义
如果一个函数能访问外部的变量,那么就形成了一个闭包。
闭包形成的原理
当一个普通函数执行结束之后,函数内的变量会被全部销毁(垃圾回收)。但是,如果某些变量在函数外部会被用到,那么该变量就不会被销毁,因此形成了闭包。可以看下面的例子:
function fn1() {
let a = 1
// 产生闭包
return () => {
console.log(a);
}
}
const fn2 = fn1()
fn2()
理论上,函数 fn1 执行完毕之后,变量 a 会被销毁。但是由于函数 fn2 的存在,在今后调用 fn2 的时候,会使用到变量 a。 因此, 变量 a 被保存下来了(没有被销毁)。这就形成了一个闭包。
假设 fn1 返回的箭头函数内,没有使用到变量 a 。那么在 fn1 执行结束后,a 变量就会被销毁,也就不存在闭包了。
闭包的应用场景
私有变量
下面示例中,count 本应该在函数结束时销毁。但是,由于increment() 使用了 count 变量,导致后续每次调用increment() 时会被使用到该变量,因此 count 不会被销毁。
在本例中,count 是一个私有变量。 外部无法直接修改它的值, 只能通过 counterModule 暴露出来的函数 increment() 来修改它的值。
const counterModule = (function () {
let count = 0;
function increment() {
count++;
console.log(count);
}
return {
increment: increment,
};
})();
counterModule.increment(); // 输出 1
counterModule.increment(); // 输出 2
函数的柯里化
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
console.log(args);
return fn(...args);
} else {
return function (...args2) {
return curried(...args.concat(args2));
};
}
};
}
curried 函数形成了一个闭包,因为它可以访问外部函数 curry 中的 fn 参数。当 curried 函数在 curry 函数作用域之外执行时,它仍然可以访问 fn,这是闭包的典型特征。
防抖和截流
myTimer 在 debounce 函数执行结束后,仍未被销毁,这是典型的闭包特征。
function debounce(fn) {
let myTimer = null;
return function () {
if (myTimer) {
clearTimeout(myTimer);
myTimer = setTimeout(() => {
myTimer = null;
console.log("3s已到,可以再次点击");
}, 3000);
} else {
fn();
myTimer = setTimeout(() => {
myTimer = null;
}, 3000);
}
};
}
循环时保存i
for (var i = 0; i < 5; i++) {
(function (i) {
setTimeout(() => {
console.log(i + 1);
}, 100);
})(i);
}
闭包的缺陷(问题)
事件监听
largeArray 是个超长数组,由于在回调函数(click) 中使用到该数组,因此该数组不会被销毁。除非取消监听。
function addEventListeners(elements) {
const largeArray = new Array(1000000).fill('large data');
elements.forEach((element) => {
element.addEventListener('click', () => {
console.log('Element clicked:', element);
console.log(largeArray[0]);
});
});
}
const elements = document.querySelectorAll('.my-element');
addEventListeners(elements);
全局变量
在下面例子中,foo 是一个全局变量,在 fn() 执行结束之后,仍然不会被销毁。因此占用大量内存。
function createBigData() {
const bigData = [];
for(let j = 0; j < 100; j++) {
bigData.push(new Array(10000).join("this_is_a_big_data"));
}
return bigData;
}
function fn() {
foo = createBigData(); // 意外的全局变量导致内存泄漏
}
for (let j = 0; j < 100; j++) {
fn();
}