1. 什么是闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
举个栗子,createCounter 接受一个参数 n,然后返回一个匿名函数,这个匿名函数是闭包,它可以访问外部函数 createCounter 的局部变量 n。因为这个内部函数在外部有被引用,该函数会不会被销毁,n的值也会被保存。
function createCounter(n) {
return function () {
return n++
};
};
const a = createCounter(1)
a()//1
a()//2
function createCounter2() {
let n = 1
return function increment() {
console.log(++n)
};
};
const b = createCounter2()
b()//2
b()//3
return返回的函数那个函数外部有引用,才会被保存;而show函数每次被调用都会重新被加载。
function createCounter3() {
return function () {
let n = 1
function show(){
console.log(++n)
}
show()
};
};
const c = createCounter3()
c()//2
c()//2
function createCounter4() {
return function () {
let n = 1
return function show(){
console.log(++n)
}
};
};
const d = createCounter4()()//show函数外部有引用
d()//2
d()//3
2. 作用域链
理解作用域链的创建和使用,对理解闭包非常重要。
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);
这里定义的 compare()函数是在全局上下文中调用的。
在定义函数时,就会为它创建作用域链,预装载全局变量对象,并保存在内部的[[Scope]]中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的[[Scope]]来创建其作用域链。
第一次调用 compare()时,会为它创建一个包含 arguments、value1 和 value2 的活动对象,这个对象是其作用域链上的第一个对象。什么是arguments对象?
compare()作用域链上的第二个对象是全局上下文的变量对象,其中包含 this、result 和 compare。
全局上下文中的叫变量对象,它会在代码执行期间始终存在。比如在浏览器中是 window
对象。
函数局部上下文中的叫活动对象,只在函数执行期间存在。当函数执行完毕后,活动对象会被销毁。
函数内部代码在访问变量时,会从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。不过,闭包就不一样了。
在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。
因此,在createCounter()函数中,匿名函数的作用域链中实际上包含createCounter的活动对象(也就是arguments和它的形参),所以在内部的函数可以访问到外部的参数。因为匿名函数中有对n的引用,所以执行完毕后createCounter不会被销毁。
function createCounter(n) {
return function () {
return n++
};
};
//1.创建函数
const a = createCounter(1)
//2.调用函数
a()//1
a()//2
//3.除对函数的引用,这样就可以释放内存了
a = null
创建的createCounter 函数被保存在变量 a 中。把 a设置为等于 null 会解除对函数的引用,从而让垃圾回收程序可以将内存释放掉,作用域链也会被销毁。
3. 闭包的优缺点
闭包的优点
1. 封装性
闭包允许创建私有变量,这对于封装和隐藏实现细节非常有用。通过在函数内部定义变量,并返回一个访问这个变量的函数,可以创建一个私有作用域。
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 输出 1
console.log(counter()); // 输出 2
2. 模块化开发
闭包可用于实现模块化开发,创建私有作用域,防止变量污染全局命名空间。
const module = (function() {
let privateVariable = 'I am private';
return {
getPrivateVariable: function() {
return privateVariable;
},
setPrivateVariable: function(value) {
privateVariable = value;
}
};
})();
console.log(module.getPrivateVariable()); // 输出 'I am private'
module.setPrivateVariable('Updated value');
console.log(module.getPrivateVariable()); // 输出 'Updated value'
3. 事件处理程序
在事件处理程序中,闭包可以用来维持对外部作用域的引用,以便在事件触发时访问外部变量。
function setupEventListener() {
let count = 0;
document.getElementById('myButton').addEventListener('click', function() {
count++;
console.log(`Button clicked ${count} times`);
});
}
setupEventListener();
4. setTimeout 和 setInterval
在定时器中使用闭包,可以保存定时器内的变量状态,而不受外部环境的影响。
function startTimer() {
let seconds = 0;
const timer = setInterval(function() {
seconds++;
console.log(`Timer: ${seconds} seconds`);
}, 1000);
return function stopTimer() {
clearInterval(timer);
};
}
const stopTimer = startTimer();
// ...一些代码后
stopTimer(); // 停止定时器
5. 在循环中使用闭包
在循环中使用闭包,可以保持对每次迭代的独立作用域,防止变量共享问题。
for (let i = 1; i <= 5; i++) {
setTimeout(function() {
console.log(`Delayed log: ${i}`);
}, i * 1000);
}
6. 缓存函数结果
可以实现对函数调用结果的缓存,提高性能。
function memoize(fn) {
const cache = {};
return function(...args) {
const key = JSON.stringify(args);
if (cache[key]) {
console.log('Result retrieved from cache');
return cache[key];
} else {
const result = fn(...args);
cache[key] = result;
return result;
}
};
}
const memoizedAdd = memoize(function(x, y) {
console.log('Performing expensive calculation');
return x + y;
});
console.log(memoizedAdd(2, 3)); // 输出 'Performing expensive calculation' 和 5
console.log(memoizedAdd(2, 3)); // 输出 'Result retrieved from cache' 和 5
闭包的缺点
- 内存占用:
- 闭包可能导致内存泄漏,因为闭包中的函数引用了外部函数的变量,导致外部函数的作用域无法被垃圾回收。
- 性能影响:
- 闭包的使用可能对性能产生一些影响,尤其是在涉及大量闭包的场景中。每个闭包都会创建一个新的作用域链,可能会导致额外的计算成本。
- 复杂性:
- 过度使用闭包可能导致代码复杂性增加,降低代码的可读性和可维护性。在某些情况下,闭包的嵌套可能会变得难以理解。
- 变量共享问题:
- 闭包可以访问外部函数的变量,这可能导致变量共享的问题。在循环中创建闭包时,需要注意变量在每次迭代中的值。
- 潜在的安全问题:
- 如果闭包中的函数依赖于外部作用域的变量,并且这些变量在函数调用时可能被修改,可能会导致潜在的安全问题。