什么是闭包?原理与应用

什么是闭包?原理与应用

闭包(Closure)是 JavaScript 中最常被提及的概念之一:它既是面试高频考点,也是诸多高级特性(如柯里化、模块化、私有变量、函数工厂等)的基础。

本文将回答三个核心问题:

  • 闭包的准确定义是什么?
  • 为什么会产生闭包?它依赖的语言特性有哪些?
  • 在工程实践中,闭包有哪些典型应用与常见坑?

一、闭包的定义

经典定义(MDN 版本,简化):

闭包是指 函数 与其 词法环境 的组合,这个环境包含了该函数在创建时可访问的所有外部变量。

更直观地说:

当一个函数在其词法作用域之外被调用时,仍然“记得”它定义时所在的作用域,这种能力就是闭包。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
function makeCounter() {
let count = 0;

return function () {
count++;
console.log(count);
};
}

const counter = makeCounter();
counter(); // 1
counter(); // 2

makeCounter 执行结束后,按理说 count 应该被销毁,但由于返回的函数中还在使用它,JavaScript 引擎会让 count 所在的词法环境继续存活,这就是闭包。


二、形成闭包的两个前提

  1. 词法作用域(Lexical Scope)
    • JavaScript 采用词法作用域:函数的作用域在定义时就决定了,而不是调用时。
  2. 函数是一等公民(First-Class Function)
    • 函数可以作为返回值,从一个作用域“带着环境”一起被返回出去。

只要满足:

  • 内部函数引用了外部函数作用域中的变量;
  • 并且这个内部函数在外部被调用;

就形成了闭包。


三、典型例子:保存状态的函数

1
2
3
4
5
6
7
8
9
10
11
function createAdder(x) {
return function (y) {
return x + y;
};
}

const add10 = createAdder(10);
const add5 = createAdder(5);

console.log(add10(3)); // 13
console.log(add5(3)); // 8

这里:

  • 每次调用 createAdder 都会创建一个独立的词法环境,其中 x 的值被固定下来
  • 返回的函数在之后调用时,仍能访问各自环境中的 x

这就是闭包在“函数工厂”“柯里化”等场景中的基础。


四、常见应用场景

1. 模拟私有变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createCounter() {
let count = 0; // 外部无法直接访问

return {
increment() {
count++;
},
getValue() {
return count;
},
};
}

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

count 对外是“私有”的,只能通过公开方法访问/修改。

2. 封装模块(IIFE 模式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const module = (function () {
const privateVar = 42;

function privateFn() {}

function publicFn() {
console.log(privateVar);
}

return {
publicFn,
};
})();

module.publicFn();

通过立即执行函数表达式(IIFE)创建一个作用域环境,把内部变量“藏起来”,只暴露所需的 API。

3. 在循环中正确捕获索引

1
2
3
4
5
6
7
8
for (var i = 0; i < 3; i++) {
(function (i) {
setTimeout(() => {
console.log(i);
}, 0);
})(i);
}
// 输出:0 1 2

i 被作为参数传入 IIFE,每次形成一个新的词法环境,从而避免所有回调共享同一个 i

在使用 let 形成块级作用域后,这种“闭包式 IIFE”用得少了,但仍有助于理解原理:

1
2
3
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 0 1 2
}

五、闭包与内存:常见误区与泄漏风险

1. 闭包本身不是内存泄漏

只要仍然有引用指向闭包函数,对应的词法环境就会被保留,这是正常行为。

只有在:

  • 闭包中持有了大量不再需要的数据/DOM 引用;
  • 但闭包对象仍被外部长期引用;

才会出现“某些本可释放的资源被错误持有”的情况。

2. 常见风险示例

1
2
3
4
5
6
7
function registerHandlers() {
const bigData = new Array(1000000).fill("xxx");

button.addEventListener("click", () => {
console.log(bigData.length);
});
}
  • 若多次调用 registerHandlers 而不移除旧的监听,bigData 将被长期占用内存。

建议:

  • 在闭包中仅保留必要的数据;
  • 在组件卸载/销毁时及时移除监听与引用。

六、闭包与 this 无关(易混点)

闭包与 this 是两个不同维度的概念:

  • 闭包:关于“变量作用域”和“词法环境”
  • this:关于“函数调用方式”的绑定

箭头函数中的 this 来自外层词法作用域,但这是“this 的词法绑定”,不是闭包本身的定义;不要把二者混为一谈。


七、如何判断代码中是否存在闭包?

一个简单的判断思路:

  1. 是否有一个函数 A 在另一个函数 B 内部定义?
  2. A 是否引用了 B 中的局部变量?
  3. A 是否被 B 之外的代码调用(或保存到外部)?

如果以上三点都满足,那么可以说:A 形成了闭包,持有 B 的词法环境。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
function outer() {
const x = 1;

function inner() {
console.log(x);
}

return inner;
}

const fn = outer();
fn(); // 1,这里就用到了闭包

八、总结

闭包是 JavaScript 非常核心的概念,可以概括为:

  • 从原理上:函数 + 其定义时的词法环境(其中包含外部变量的引用)
  • 从表现上:函数在其词法作用域之外执行时,仍能访问定义时的外部变量
  • 从应用上:用来实现私有变量、模块封装、函数工厂、柯里化等高级模式

理解闭包,关键是把握“函数定义时的环境会被记住”这一点,再结合作用域链、垃圾回收等知识,你就能从容应对面试与实际开发中的所有闭包相关问题。


什么是闭包?原理与应用
https://sunjc.vip/2024/10/29/什么是闭包及其应用/
作者
Sunjc
发布于
2024年10月29日
许可协议