JavaScript闭包的工作原理

JavaScript闭包的工作原理

技术背景

在JavaScript中,闭包是一个非常重要且强大的特性。它使得函数能够访问并操作其外部作用域中的变量,即使外部函数已经执行完毕。这为数据隐藏、封装以及实现一些高级编程模式提供了可能。在2015年之前,JavaScript没有类语法,也没有私有字段语法,闭包在一定程度上弥补了这些不足。

实现步骤

理解闭包的基本概念

闭包是函数和对其外部作用域(词法环境)的引用的组合。词法环境是每个执行上下文的一部分,它是标识符(如局部变量名)和值之间的映射。

创建闭包

以下是一个简单的闭包示例:

1
2
3
4
5
6
7
8
function foo() {
const secret = Math.trunc(Math.random() * 100);
return function inner() {
console.log(`The secret number is ${secret}.`);
};
}
const f = foo();
f();

在这个例子中,inner 函数形成了一个闭包,它引用了 foo 函数执行时创建的词法环境中的 secret 变量。

闭包的使用场景

私有实例变量

1
2
3
4
5
6
7
8
9
10
function Car(manufacturer, model, year, color) {
return {
toString() {
return `${manufacturer} ${model} (${year}, ${color})`;
}
};
}

const car = new Car('Aston Martin', 'V8 Vantage', '2012', 'Quantum Silver');
console.log(car.toString());

在这个例子中,toString 函数关闭了 Car 函数的细节,形成了闭包。

函数式编程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function curry(fn) {
const args = [];
return function inner(arg) {
if (args.length === fn.length) return fn(...args);
args.push(arg);
return inner;
};
}

function add(a, b) {
return a + b;
}

const curriedAdd = curry(add);
console.log(curriedAdd(2)(3)());

这里的 inner 函数关闭了 fnargs,形成闭包。

事件驱动编程

1
2
3
4
5
6
7
8
const $ = document.querySelector.bind(document);
const BACKGROUND_COLOR = 'rgba(200, 200, 242, 1)';

function onClick() {
$('body').style.background = BACKGROUND_COLOR;
}

$('button').addEventListener('click', onClick);

onClick 函数关闭了 BACKGROUND_COLOR 变量,形成闭包。

模块化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let namespace = {};

(function foo(n) {
let numbers = [];

function format(n) {
return Math.trunc(n);
}

function tick() {
numbers.push(Math.random() * 100);
}

function toString() {
return numbers.map(format);
}

n.counter = {
tick,
toString
};
}(namespace));

const counter = namespace.counter;
counter.tick();
counter.tick();
console.log(counter.toString());

在这个例子中,ticktoString 函数关闭了它们完成工作所需的私有状态和函数,实现了代码的模块化和封装。

核心代码

基本闭包示例

1
2
3
4
5
6
7
8
9
10
function outer(x) {
var tmp = 3;
return function inner(y) {
console.log(x + y + (++tmp));
};
}

var bar = outer(2);
bar(10);
bar(10);

闭包用于统计点击次数

1
2
3
4
5
6
7
8
9
10
11
12
var element = document.getElementById('button');

element.addEventListener("click", (function () {
var count = 0;
return function (e) {
count++;
if (count === 3) {
console.log("Third time's the charm!");
count = 0;
}
};
})());

最佳实践

  • 数据隐藏和封装:使用闭包来隐藏函数内部的变量,防止外部代码直接访问和修改。
  • 函数复用:通过闭包可以创建具有特定状态的函数,这些函数可以在不同的地方复用。
  • 事件处理:在事件处理函数中使用闭包来保存事件处理所需的状态。

常见问题

变量作用域问题

使用 var 声明变量时,由于变量提升,可能会导致闭包引用的变量值不符合预期。建议使用 letconst 来声明变量。

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

return result;
}

const result = foo();
for (var i = 0; i < 3; i++) {
result[i]();
}

在这个例子中,所有的 inner 函数都引用了同一个 i 变量,最终输出的都是 3

内存泄漏问题

在IE浏览器中,如果DOM元素引用了闭包,而闭包又引用了DOM元素,可能会导致内存泄漏。在现代浏览器中,JavaScript会自动清理不再引用的循环结构,但在使用时仍需注意。


JavaScript闭包的工作原理
https://119291.xyz/posts/2025-05-07.how-do-javascript-closures-work/
作者
ww
发布于
2025年5月7日
许可协议