闭包

2024-07-17

闭包 (Closure) 笔记

1. 什么是闭包

闭包是 JavaScript 中的一种函数结构,它允许函数“记住”并访问定义在它外部的变量,即使外部函数已经执行结束。

换句话说,闭包是函数与其词法作用域的组合。


2. 闭包的形成条件

闭包通常由以下三个条件形成:

  1. 函数嵌套:内部函数定义在外部函数内部。
  2. 访问外部变量:内部函数使用了外部函数的变量。
  3. 外部函数返回内部函数或将其赋值给其他作用域:使内部函数在外部函数执行后仍然可访问外部变量。
1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}

const fn = outer();
fn(); // 1
fn(); // 2

解释:inner 能访问 outercount,形成闭包。


3. 闭包的作用与用途

3.1 数据封装与私有化

闭包可以模拟“私有变量”,保护数据不被外部直接访问。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createCounter() {
let count = 0; // 私有变量
return {
increment: function() {
count++;
return count;
},
decrement: function() {
count--;
return count;
}
};
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.decrement()); // 1

3.2 延迟执行 & 回调

闭包可以延迟访问变量的值,用于回调函数、事件处理等。

1
2
3
4
5
6
7
8
function delayMessage(msg) {
return function() {
console.log(msg);
};
}

const showHello = delayMessage('Hello World!');
setTimeout(showHello, 1000); // 1秒后输出 "Hello World!"

3.3 维持状态

闭包可以让函数记住状态信息,适合实现状态机、计数器等功能。


4. 闭包的注意事项

  1. 内存泄漏

    • 闭包会保持对外部作用域变量的引用,导致这些变量不会被垃圾回收。
    • 尤其在大量动态创建闭包的场景下,需要注意释放不必要的引用。
  2. 性能问题

    • 不当使用闭包可能导致额外内存开销。
    • 尽量避免在循环中直接创建大量闭包,或者使用 let 循环变量+立即执行函数优化。
  3. 循环中闭包问题

    • ES5 var 声明变量时容易陷入经典闭包陷阱。
1
2
3
4
5
6
7
8
9
10
11
12
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3
}, 100);
}

// 解决方法:使用 IIFE 或 let
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}

5. 闭包原理(深入理解)

  1. 词法作用域
    • JS 函数会根据定义的位置确定其作用域链。
  2. 函数引用外部变量
    • 当闭包存在时,外部函数的作用域不会销毁,内部函数持续持有引用。
  3. 作用域链与执行上下文
    • 内部函数访问变量时,会沿作用域链查找,直到找到变量或到达全局作用域。

6. 总结

  • 闭包 = 函数 + 作用域链
  • 能够访问外部函数的变量,即使外部函数已经执行结束。
  • 常用于:
    • 数据封装与私有化
    • 延迟执行或回调
    • 维护状态(计数器、缓存等)
  • 使用闭包时要注意:
    • 内存泄漏:避免持有不必要的引用
    • 循环变量陷阱:使用 let 或 IIFE 避免共享同一变量
    • 性能开销:避免大量动态创建闭包

闭包是 JavaScript 核心概念之一,理解闭包对于模块化、函数式编程以及异步编程非常关键。


7. 练习题

  1. 使用闭包实现一个计数器,支持 add()reset() 方法。
  2. 写一个函数,返回一个数组,每个元素是 0~4 的平方值,要求使用闭包解决循环变量问题。
  3. 分析下面代码输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
result.push(function() {
console.log(i);
});
}
return result;
}

var funcs = createFunctions();
funcs[0]();
funcs[1]();
funcs[2]();

8. 面试题解析

题目分析:
上面的代码中,使用 var 声明循环变量 i,所有函数都共享同一个 i,循环结束时 i = 3,所以三次调用输出都是 3

解决方法 1:使用 let

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createFunctions() {
var result = [];
for (let i = 0; i < 3; i++) {
result.push(function() {
console.log(i);
});
}
return result;
}

var funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

let 每次循环都会创建新的块级作用域,闭包绑定的是每次循环的 i

解决方法 2:使用 IIFE(立即执行函数表达式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createFunctions() {
var result = [];
for (var i = 0; i < 3; i++) {
(function(j) {
result.push(function() {
console.log(j);
});
})(i);
}
return result;
}

var funcs = createFunctions();
funcs[0](); // 0
funcs[1](); // 1
funcs[2](); // 2

通过 IIFE 将当前循环变量 i 传入参数 j,每次闭包绑定的都是独立的值。

总结面试要点:

  • 闭包常考点:私有变量、状态保存、异步回调。
  • 循环中闭包容易出现共享同一变量的问题。
  • 面试时应能解释 varlet 的区别,以及如何使用 IIFE 或块级作用域解决闭包问题。