作用域和闭包
作用域(Scope)和闭包(Closure)是 JavaScript 的核心概念,理解它们是掌握 JS 动态特性的关键。以下是深入浅出的解析:
一、作用域(Scope)
1. 定义与分类
- 作用域:变量和函数的可访问范围,控制着变量与函数的生命周期。
- 分类:
- 全局作用域:最外层作用域,所有未定义在函数或块内的变量都属于全局作用域。
- 函数作用域:函数内部定义的变量只能在函数内部访问(
var
)。 - 块级作用域:
let
和const
声明的变量在{}
内有效(如if
、for
块)。
2. 变量提升(Hoisting)
var
声明的变量会被提升到作用域顶部,但赋值不会提升。let
和const
存在暂时性死区(TDZ),声明前无法访问。
console.log(a); // undefined(变量提升)
var a = 1;
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 2;
3. 词法作用域(静态作用域)
- JS 采用词法作用域,函数的作用域在定义时确定,而非调用时。
const x = 10;
function outer() {
console.log(x); // 10(从定义位置查找 x)
}
function inner() {
const x = 20;
outer(); // 输出 10,而非 20
}
inner();
二、闭包(Closure)
1. 定义与本质
- 闭包:函数与其引用的词法环境的组合。即使函数执行完毕,其作用域内的变量也不会被销毁。
- 核心条件:
- 函数嵌套。
- 内部函数引用外部函数的变量或参数。
2. 经典示例
function outer() {
const count = 0;
function inner() {
count++; // 引用外部变量
console.log(count);
}
return inner; // 返回闭包
}
const counter = outer();
counter(); // 1
counter(); // 2(保留了上次的状态)
3. 闭包的特性
- 捕获变量:闭包会捕获其定义时的整个词法环境,而非变量的值。
function createGreeters() {
const greeters = [];
for (var i = 0; i < 3; i++) {
greeters.push(() => console.log(i)); // 捕获变量 i 的引用
}
return greeters;
}
const [g1, g2, g3] = createGreeters();
g1(); // 3(循环结束时 i=3)
g2(); // 3
g3(); // 3
// 使用 let 修复(块级作用域)
for (let i = 0; i < 3; i++) {
// 每次循环创建独立的 i
greeters.push(() => console.log(i));
}
三、闭包的应用场景
1. 数据封装与私有变量
function createCounter() {
let count = 0; // 私有变量
return {
increment() { count++; },
getCount() { return count; }
};
}
const counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 1
console.log(counter.count); // undefined(无法直接访问私有变量)
2. 函数柯里化(Currying)
function add(a, b) {
return a + b;
}
// 转为柯里化函数
const curriedAdd = a => b => a + b;
curriedAdd(3)(5); // 8
3. 事件处理与回调
function setupButton() {
const text = "Hello";
document.getElementById("btn").addEventListener("click", () => {
console.log(text); // 闭包捕获 text
});
}
4. 防抖与节流(见前文解析)
四、闭包的注意事项
1. 内存泄漏风险
- 闭包会保留对外部变量的引用,若闭包长期存在(如全局变量),可能导致内存无法释放。
// 错误示例:闭包引用 DOM 导致内存泄漏
function leakMemory() {
const element = document.getElementById("leaky");
// 闭包一直引用 element
document.addEventListener("click", () => {
console.log(element.innerHTML);
});
}
2. 性能优化
- 避免在循环中创建闭包,若无法避免,使用
let
创建块级作用域。 - 及时释放不再需要的闭包引用(如置为
null
)。
五、作用域链(Scope Chain)
- 定义:由多个嵌套的作用域组成的链表,用于查找变量和函数。
- 工作原理:当访问一个变量时,JS 会先在当前作用域查找,若未找到则沿作用域链向上查找,直到全局作用域。
const globalVar = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // 自身作用域
console.log(outerVar); // 外层作用域
console.log(globalVar); // 全局作用域
}
inner();
}
outer();
六、常见面试问题
-
什么是闭包?为什么需要闭包?
- 答:闭包是函数与其词法环境的组合,用于实现数据封装、状态保持和函数复用。
-
闭包会导致内存泄漏吗?如何避免?
- 答:若闭包长期持有对 DOM 或大对象的引用,可能导致泄漏。应避免不必要的引用,并在不再需要时释放闭包。
-
如何理解 JS 的词法作用域?
- 答:函数的作用域由其定义位置决定,而非调用位置。
-
分析以下代码的输出:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1000);
}- 答:输出
3 3 3
。因为var
声明的i
是函数作用域,所有定时器共享同一个i
,循环结束时i
已变为 3。使用let
可修复此问题。
- 答:输出
七、总结
- 作用域决定了变量的可见性和生命周期,JS 有全局、函数和块级作用域。
- 闭包是 JS 最强大的特性之一,允许函数访问并保留其外部作用域的变量,即使外部函数已执行完毕。
- 合理使用闭包可实现数据封装、状态管理和高阶函数,但需警惕内存泄漏风险。
理解作用域和闭包是掌握 JS 异步编程、函数式编程和框架原理(如 React Hooks)的基础。