JS 闭包 (Closure)

三葉Leaves Author

概要

是什么

内层函数 + 外层作用域

做什么

在 JavaScript 中,对象并没有像 Java 或 C++ 那样提供真正的私有属性。但通过闭包,我们可以模拟实现私有变量的效果。

  • 封闭数据,实现数据私有,使得外部也可以访问函数内部的变量。

其他的一些作用:

  • 函数工厂 (Function Factories): 闭包可以用来创建一系列相似但又略有不同的函数
  • 回调函数与事件处理 (Callbacks and Event Handlers): 在异步编程,尤其是处理回调函数和事件监听器时,闭包非常有用,它可以帮助我们保持状态。

注意什么

  • 潜在的内存消耗 和内存泄露。
  • 循环中的变量引用问题。

实例

计数器演示私有变量效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function outer() {
let i = 0;
function fn(){
i++
console.log(i);
}
// 返回的是一个函数,这个函数每次调用,私有变量都自增1
return fn;
}

const ok = outer();

// 下面演示用户在 DevTools 里试图修改函数私有变量 i 的值,但是并不造成影响
ok()
i = 10000
console.log(i);
ok()
console.log(i);
ok()

函数工厂实例

1
2
3
4
5
6
7
8
9
10
11
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}

let double = createMultiplier(2); // 创建一个“乘以2”的函数
let triple = createMultiplier(3); // 创建一个“乘以3”的函数

console.log(double(5)); // 输出: 10
console.log(triple(5)); // 输出: 15

这里,createMultiplier 是一个函数工厂。它接收一个 factor 参数,并返回一个新的函数。这个返回的函数就是一个闭包,它“记住”了传递给 createMultiplierfactor 值。

回调函数与事件处理实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 假设我们有一个按钮
// <button id="myButton">Click Me</button>

function setupButtonClickHandler() {
let clickCount = 0;
const button = document.getElementById('myButton');

if (button) {
button.addEventListener('click', function() { // 这个匿名函数是一个闭包
clickCount++;
console.log(`Button clicked ${clickCount} times.`);
});
}
}

setupButtonClickHandler();
// 每次点击按钮,clickCount 都会增加,因为它被闭包“记住”了。

不同于直接定义一个事件监听器,闭包方式可以把状态(clickCount)和逻辑(按钮点击)封装在一起,避免全局污染,适合模块化和面向对象开发。
在大型项目或多人协作时,推荐使用闭包(第二种写法),这样更安全、可维护性更高。

注意点

内存消耗

由于闭包会使其外部函数的变量一直保存在内存中,如果滥用闭包,或者闭包引用的外部变量占用内存过大,可能会导致内存消耗增加。 详情可见 GC 机制和内存泄露

不过,现代 JavaScript 引擎在垃圾回收方面已经做得相当出色,通常情况下不必过分担心,但了解这一点总是有益的。

循环中的闭包问题

下面是一道面试题,这是一个非常经典的例子,尤其是在使用 var 声明循环变量时。

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); // 这个 i 引用的是循环结束后的 i
});
}
return result;
}

var funcs = createFunctions();
funcs[0](); // 输出: 3
funcs[1](); // 输出: 3
funcs[2](); // 输出: 3

为什么都是 3?因为循环体内的匿名函数(闭包)共享同一个词法作用域,它们引用的都是同一个变量 i。当循环结束后,i 的值变成了 3。所以,当这些函数被调用时,它们访问到的 i 都是 3。

如何解决这个问题?

  • 方法一:使用立即执行函数表达式 (IIFE) 创建新的作用域
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createFunctionsFixed_IIFE() {
var result = [];
for (var i = 0; i < 3; i++) {
(function(j) { // 使用 IIFE,并传入当前的 i 作为参数 j
result.push(function() {
console.log(j);
});
})(i); // 立即执行,并将当前的 i 值传递进去
}
return result;
}
var funcsFixed_IIFE = createFunctionsFixed_IIFE();
funcsFixed_IIFE[0](); // 输出: 0
funcsFixed_IIFE[1](); // 输出: 1
funcsFixed_IIFE[2](); // 输出: 2
  • 方法二:使用 letconst (ES6 推荐) ES6 引入的 letconst 具有块级作用域,它们在循环中会为每次迭代创建一个新的绑定。
1
2
3
4
5
6
7
8
9
10
11
12
13
function createFunctionsFixed_Let() {
const result = [];
for (let i = 0; i < 3; i++) { // 使用 let 声明 i
result.push(function() {
console.log(i);
});
}
return result;
}
const funcsFixed_Let = createFunctionsFixed_Let();
funcsFixed_Let[0](); // 输出: 0
funcsFixed_Let[1](); // 输出: 1
funcsFixed_Let[2](); // 输出: 2
  • 标题: JS 闭包 (Closure)
  • 作者: 三葉Leaves
  • 创建于 : 2025-06-01 00:00:00
  • 更新于 : 2025-06-07 19:46:13
  • 链接: https://blog.oksanye.com/da281a1baf24/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论