JavaScript闭包工作原理详解

JavaScript闭包工作原理详解

技术背景

在JavaScript编程中,闭包是一个非常重要且强大的特性。对于有函数、变量等基本概念基础,但不理解闭包的开发者来说,闭包可能显得有些神秘。闭包允许函数访问并操作其外部作用域中的变量,即使该外部函数已经执行完毕。这一特性为数据隐藏、封装以及函数状态的持久化提供了有力的支持。在早期,JavaScript没有类语法和私有字段语法,闭包成为了实现这些功能的重要手段。

实现步骤

1. 理解闭包的基本概念

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

2. 创建一个简单的闭包示例

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(); // `secret` 不能从 `foo` 外部直接访问
f(); // 唯一能获取 `secret` 的方法是调用 `f`

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

3. 应用闭包实现特定功能

私有实例变量

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)()); // 5

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
11
12
function outerFunction() {
var outerVar = "monkey";

function innerFunction() {
alert(outerVar);
}

return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

闭包在循环中的应用(正确方式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
return function () {
return 'n = ' + n;
}
}

for (var index = 0; index < 10; index++) {
arr[index] = createClosure(index);
}

for (var index of arr) {
console.log(arr[index]());
}

最佳实践

1. 数据隐藏和封装

使用闭包可以将数据隐藏在函数内部,只提供必要的接口来访问和修改数据,从而实现数据的封装。

2. 函数状态的持久化

闭包可以让函数记住其执行时的状态,使得函数在多次调用之间能够保持数据的连续性。

3. 模块化开发

通过闭包可以将相关的功能封装在一个模块中,避免全局变量的污染,提高代码的可维护性和可复用性。

常见问题

1. 变量声明使用 var 导致的问题

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

return result;
}

const result = foo();
// 以下将打印 `3` 三次...
for (var i = 0; i < 3; i++) {
result[i]();
}

由于 var 声明的变量会被提升到函数作用域的顶部,所有的 inner 函数都关闭了同一个 i 变量,导致最终打印的都是 3。在现代JavaScript中,建议使用 letconst 来声明变量。

2. 闭包导致的内存泄漏问题

在IE浏览器中,如果不注意,闭包可能会导致内存泄漏。当DOM元素的属性值引用了闭包,而IE未能正确断开这些引用时,就会导致内存无法被释放。在其他现代浏览器中,JavaScript会自动清理不再被引用的循环结构。

3. 对闭包创建时机的误解

闭包不是只有在返回内部函数时才会创建。实际上,只要内部函数可以访问外部函数的词法环境,闭包就已经存在。例如,将内部函数赋值给外部作用域的变量,或者将其作为参数传递给其他函数,都可能会创建闭包。


JavaScript闭包工作原理详解
https://119291.xyz/posts/2025-04-16.javascript-closures-explained/
作者
ww
发布于
2025年4月16日
许可协议