闭包与作用域链
1. 作用域
作用域 是指变量和函数的可访问范围。JavaScript 中主要有三种作用域:
- 全局作用域:在该作用域声明的变量在代码任何地方都能被访问。
- 函数作用域:在函数内部声明的变量只能在函数内部访问。
- 块级作用域:ES6 引入了
let 和 const,使 {} 包裹的代码块也形成作用域。
2. 作用域链
当查找变量时,JavaScript 引擎会沿着作用域链向上查找,直到找到变量或到达顶层作用域(全局作用域)。如果在全局作用域中仍未找到,则抛出 ReferenceError。
作用域链是在函数定义时确定的,而不是在函数调用时。这就是所谓的词法作用域。
3. 闭包
闭包 是指一个函数能够访问其词法作用域中的变量,即使该函数在其词法作用域之外被调用。
简单来说:闭包 = 函数 + 该函数能访问的自由变量。
核心特性
- 函数嵌套:必须有函数嵌套函数。
- 内部函数引用外部变量:内部函数引用了外部函数的变量。
- 外部函数返回内部函数:外部函数执行完毕后,其执行环境虽然销毁了,但其变量对象因为被内部函数引用而无法释放,这就形成了闭包。
示例
function createCounter() {
let count = 0; // 私有变量
return function() {
count++;
console.log(count);
};
}
const counter = createCounter();
counter(); // 1
counter(); // 2
在这个例子中:
createCounter 执行完毕后,count 变量本应被回收。
- 但因为返回的匿名函数引用了
count,形成了闭包。
count 变量一直保存在内存中,直到 counter 被销毁。
4. 闭包的应用场景
(1) 数据私有化 / 模拟私有变量
利用闭包,我们可以隐藏变量,只通过特定的 API 暴露操作接口,防止全局污染和外部直接修改。
const Person = (function() {
let _name = 'Unknown'; // 私有变量
function Person(name) {
_name = name;
}
Person.prototype.getName = function() {
return _name;
};
return Person;
})();
(2) 柯里化
将多参数函数转换为一系列单参数函数的过程。
function add(x) {
return function(y) {
return x + y;
};
}
const add5 = add(5);
console.log(add5(3)); // 8
(3) 防抖和节流
这是前端最常见的闭包应用。
function debounce(fn, delay) {
let timer = null;
return function(...args) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
5. 闭包与内存泄漏
闭包的主要缺点是内存占用。由于闭包会引用外部函数的变量,导致这些变量无法被垃圾回收机制回收。
如何避免内存泄漏?
- 尽量避免不必要的闭包。
- 在不再需要闭包时,手动解除引用(例如将变量赋值为
null)。
let element = document.getElementById('button');
element.onclick = function() {
console.log(element.id);
};
// 循环引用:element -> onclick -> function -> element
// 解决办法:
// element.onclick = null;
// element = null;
6. 面试常见问题
Q: 循环中的闭包陷阱?
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 1000); // 输出 5 个 5
}
原因:var 只有函数作用域,循环结束时 i 已经是 5。回调函数引用的是同一个 i。
解决:
- 使用
let (推荐):let 有块级作用域,每次循环都会创建一个新的绑定。
- IIFE (立即执行函数):构造一个新的函数作用域来捕获当前的
i。
for (var i = 0; i < 5; i++) {
(function(j) {
setTimeout(() => console.log(j), 1000);
})(i);
}