【JavaScript-Day 20】揭秘函数的“记忆”:深入浅出理解闭包(Closure)
Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
01-【JavaScript-Day 1】从零开始:全面了解 JavaScript 是什么、为什么学以及它与 Java 的区别
02-【JavaScript-Day 2】开启 JS 之旅:从浏览器控制台到 <script>
标签的 Hello World 实践
03-【JavaScript-Day 3】掌握JS语法规则:语句、分号、注释与大小写敏感详解
04-【JavaScript-Day 4】var
完全指南:掌握变量声明、作用域及提升
05-【JavaScript-Day 5】告别 var
陷阱:深入理解 let
和 const
的妙用
06-【JavaScript-Day 6】从零到精通:JavaScript 原始类型 String, Number, Boolean, Null, Undefined, Symbol, BigInt 详解
07-【JavaScript-Day 7】全面解析 Number 与 String:JS 数据核心操作指南
08-【JavaScript-Day 8】告别混淆:一文彻底搞懂 JavaScript 的 Boolean、null 和 undefined
09-【JavaScript-Day 9】从基础到进阶:掌握 JavaScript 核心运算符之算术与赋值篇
10-【JavaScript-Day 10】掌握代码决策核心:详解比较、逻辑与三元运算符
11-【JavaScript-Day 11】避坑指南!深入理解JavaScript隐式和显式类型转换
12-【JavaScript-Day 12】掌握程序流程:深入解析 if…else 条件语句
13-【JavaScript-Day 13】告别冗长if-else:精通switch语句,让代码清爽高效!
14-【JavaScript-Day 14】玩转 for
循环:从基础语法到遍历数组实战
15-【JavaScript-Day 15】深入解析 while 与 do…while 循环:满足条件的重复执行
16-【JavaScript-Day 16】函数探秘:代码复用的基石——声明、表达式与调用详解
17-【JavaScript-Day 17】函数的核心出口:深入解析 return
语句的奥秘
18-【JavaScript-Day 18】揭秘变量的“隐形边界”:深入理解全局与函数作用域
19-【JavaScript-Day 19】深入理解 JavaScript 作用域:块级、词法及 Hoisting 机制
20-【JavaScript-Day 20】揭秘函数的“记忆”:深入浅出理解闭包(Closure)
文章目录
- Langchain系列文章目录
- Python系列文章目录
- PyTorch系列文章目录
- 机器学习系列文章目录
- 深度学习系列文章目录
- Java系列文章目录
- JavaScript系列文章目录
- 前言
- 一、什么是闭包?
- 1.1 闭包的定义
- 1.2 闭包的“记忆”是如何产生的?
- 1.2.1 词法作用域
- 1.2.2 作用域链
- 1.2.3 “记忆”的形成
- 二、闭包是如何形成的?
- 2.1 形成闭包的条件
- 2.1.1 函数嵌套
- 2.1.2 内部函数引用外部变量
- 2.1.3 外部函数返回内部函数(或内部函数被暴露)
- 2.2 一个经典的闭包示例:函数工厂
- 三、闭包的简单应用
- 3.1 封装私有变量
- 3.1.1 什么是私有变量?
- 3.1.2 使用闭包实现
- 3.2 实现回调与异步
- 四、理解闭包的注意事项
- 4.1 性能考量:内存泄漏风险
- 4.2 循环中的闭包陷阱
- (1) 使用立即执行函数表达式 (IIFE) 创建新作用域
- (2) 使用 `let` 声明变量 (ES6 推荐)
- 五、总结
前言
你好,JavaScript 探索者们! 欢迎来到本系列文章的第 20 篇。在前几篇文章中,我们深入探讨了 JavaScript 的函数以及至关重要的作用域概念(全局作用域、函数作用域、块级作用域和词法作用域)。今天,我们将踏入一个让许多初学者感到困惑,但又是 JavaScript 中极其强大且核心的概念——闭包(Closure)。
闭包就像是函数的“记忆”,它让函数能够“记住”并访问它被创建时所处的环境,即使那个环境已经“消失”了。理解闭包,不仅能帮助你写出更优雅、更模块化的代码,更是迈向 JavaScript 高手之路的关键一步。本文将带你从零开始,揭开闭包的神秘面纱,理解它的原理、形成条件,并探索它的实际应用场景。准备好了吗?让我们开始吧!
一、什么是闭包?
闭包的概念听起来可能有些抽象,但别担心,我们会用最通俗易懂的方式来解释它。
1.1 闭包的定义
闭包(Closure) 是指一个函数以及其创建时所在的词法环境(Lexical Environment)的组合。
换句话说,当一个内部函数(定义在另一个函数内部的函数)被返回或者以其他方式在其外部函数执行完毕后仍然能够被访问时,它就形成了一个闭包。这个内部函数“记住”了它诞生时的“家”——也就是它的外部函数的变量、参数等所有信息。
核心思想:即使外部函数已经执行结束,其作用域链中的变量对象也不会立即被销毁,因为内部函数(闭包)仍然保留着对它的引用。这使得内部函数无论在哪里被调用,都能访问到其定义时外部函数作用域中的变量。
1.2 闭包的“记忆”是如何产生的?
要理解闭包的“记忆”能力,我们需要回顾一下 JavaScript 的词法作用域(Lexical Scoping) 和 作用域链(Scope Chain)。
1.2.1 词法作用域
我们在第 19 篇中提到,JavaScript 采用的是词法作用域。这意味着函数的作用域在函数定义时就已经确定了,而不是在函数调用时确定。无论函数在哪里被调用,它都只能访问定义时其作用域链上的变量。
1.2.2 作用域链
当一个函数被创建时,会创建一个包含其父级作用域(以及父级的父级,直至全局作用域)的链条,这就是作用域链。当函数需要访问一个变量时,它会首先在自己的作用域中查找;如果找不到,就沿着作用域链向上查找,直到找到该变量或到达全局作用域。
1.2.3 “记忆”的形成
当一个内部函数引用了外部函数的变量时,JavaScript 引擎会确保这些被引用的变量在外部函数执行完毕后仍然存活,因为内部函数(可能在未来某个时刻被调用)需要它们。这个“存活”的外部函数环境,就和内部函数一起,构成了闭包。
让我们看一个简单的例子:
function outerFunction() {let outerVariable = "我是外部函数的变量";function innerFunction() {console.log(outerVariable); // 内部函数访问外部变量}return innerFunction; // 返回内部函数
}const myClosure = outerFunction(); // outerFunction 执行完毕,返回了 innerFunction
myClosure(); // 调用 innerFunction,输出 "我是外部函数的变量"
在这个例子中:
outerFunction
定义了outerVariable
和innerFunction
。innerFunction
访问了outerVariable
。outerFunction
返回了innerFunction
并赋值给myClosure
。- 尽管
outerFunction
已经执行完毕,但由于myClosure
(即innerFunction
) 仍然存在,并且它引用了outerVariable
,所以outerVariable
并未被销毁。 - 当我们调用
myClosure()
时,它仍然能够访问并打印出outerVariable
的值。这就是闭包的“记忆”!
二、闭包是如何形成的?
理解了闭包是什么,我们来看看形成闭包需要满足哪些条件,并通过一个经典的例子来加深理解。
2.1 形成闭包的条件
通常,一个闭包的形成需要满足以下几个关键条件:
2.1.1 函数嵌套
必须存在至少两层函数嵌套关系,即一个函数(内部函数)定义在另一个函数(外部函数)的内部。
2.1.2 内部函数引用外部变量
内部函数必须直接或间接地引用了外部函数作用域中的变量(包括参数、arguments
对象或在外部函数中定义的其他变量和函数)。
2.1.3 外部函数返回内部函数(或内部函数被暴露)
最关键的一点是,内部函数必须在外部函数执行完毕后仍然能够被访问。最常见的方式就是将内部函数作为外部函数的返回值 return
出来。当然,也可以通过将内部函数赋值给一个全局变量、作为参数传递给其他函数、或者作为事件处理函数等方式将其“暴露”出来。
2.2 一个经典的闭包示例:函数工厂
闭包一个常见的用途是创建“函数工厂”,即一个函数,它能根据传入的参数生成并返回新的、具有特定功能的函数。
function makeAdder(x) {// x 是外部函数的参数return function(y) {// 内部函数引用了外部函数的 xreturn x + y;};
}// 创建两个 "加法器" 函数
const add5 = makeAdder(5); // 这里的 add5 就是一个闭包,它记住了 x = 5
const add10 = makeAdder(10); // 这里的 add10 也是一个闭包,它记住了 x = 10console.log(add5(2)); // 输出 7 (5 + 2)
console.log(add10(2)); // 输出 12 (10 + 2)
解析:
makeAdder
是外部函数,它接收一个参数x
。- 它内部定义并返回了一个匿名函数,这个匿名函数接收一个参数
y
。 - 这个匿名函数(内部函数)引用了外部函数
makeAdder
的参数x
。 - 当我们调用
makeAdder(5)
时,makeAdder
执行并返回了内部函数。此时,一个闭包形成了。这个闭包“记住”了x
的值是5
。我们将这个闭包赋值给了add5
。 - 同理,
add10
是另一个闭包,它记住了x
的值是10
。 - 当我们调用
add5(2)
时,实际上是在执行那个记住了x=5
的内部函数,并传入y=2
,所以结果是 5 + 2 = 7 5 + 2 = 7 5+2=7。 - 调用
add10(2)
时,执行的是记住了x=10
的内部函数,结果是 10 + 2 = 12 10 + 2 = 12 10+2=12。
三、闭包的简单应用
闭包不仅仅是一个理论概念,它在实际开发中有着广泛的应用。
3.1 封装私有变量
JavaScript 在 ES6 之前并没有原生的私有变量支持,但我们可以利用闭包来模拟实现私有变量,从而创建更健壮的模块。
3.1.1 什么是私有变量?
私有变量是指那些只能在特定对象或模块内部访问,而不能从外部直接访问或修改的变量。这有助于保护数据的完整性和隐藏实现细节。
3.1.2 使用闭包实现
function createCounter() {let privateCount = 0; // 这是一个 "私有" 变量// 返回一个对象,这个对象包含了操作私有变量的方法return {increment: function() {privateCount++;console.log("Count is now:", privateCount);},decrement: function() {privateCount--;console.log("Count is now:", privateCount);},getValue: function() {return privateCount;}};
}const counter1 = createCounter();
counter1.increment(); // 输出: Count is now: 1
counter1.increment(); // 输出: Count is now: 2
console.log(counter1.getValue()); // 输出: 2
// console.log(counter1.privateCount); // 输出: undefined,无法直接访问const counter2 = createCounter();
counter2.increment(); // 输出: Count is now: 1 (counter2 有自己的 privateCount)
解析:
createCounter
函数内部定义了privateCount
。- 它返回了一个包含三个方法的对象:
increment
、decrement
和getValue
。 - 这三个方法都是在
createCounter
内部定义的,所以它们可以访问privateCount
,形成了闭包。 - 由于
privateCount
变量只存在于createCounter
的作用域内,外部无法直接访问它,只能通过返回对象提供的公共方法来间接操作它。 - 每次调用
createCounter()
都会创建一个新的作用域和新的privateCount
,因此counter1
和counter2
互不影响,拥有各自独立的计数器。
3.2 实现回调与异步
闭包在处理回调函数和异步操作(如 setTimeout
、setInterval
、事件监听、Ajax 请求等)时至关重要。它们确保了当异步操作完成并执行回调时,回调函数仍然能够访问到其定义时需要的数据。
function setupClickListener(elementId, message) {const element = document.getElementById(elementId);if (element) {element.addEventListener('click', function() {// 这个回调函数是一个闭包// 它 "记住" 了 setupClickListener 调用时的 message 参数console.log(`你点击了 ${elementId},消息是:${message}`);});}
}// 假设 HTML 中有 <button id="myButton">点击我</button>
setupClickListener('myButton', '你好,闭包!');
// 当按钮被点击时(可能在 setupClickListener 执行很久之后),
// 回调函数仍然能访问到 '你好,闭包!' 这个 message。
四、理解闭包的注意事项
虽然闭包非常强大,但在使用时也需要注意一些潜在的问题。
4.1 性能考量:内存泄漏风险
因为闭包会使其外部函数的变量对象一直保存在内存中,如果滥用闭包,或者闭包引用了庞大的对象而没有及时释放,可能会导致比预期更高的内存消耗,甚至在某些旧版浏览器或不当使用的情况下引发内存泄漏。
建议:
- 只在真正需要的时候使用闭包。
- 如果一个闭包不再需要,确保没有任何引用指向它,以便垃圾回收机制能够回收其占用的内存(例如,将引用设置为
null
)。
4.2 循环中的闭包陷阱
这是一个非常经典的面试题和实践中容易遇到的坑,尤其是在使用 var
声明循环变量时。
for (var i = 1; i <= 3; i++) {setTimeout(function() {console.log(i); // 你期望输出 1, 2, 3,但实际会输出 4, 4, 4}, 1000);
}
为什么会这样?
setTimeout
是异步的,它会让内部的函数在约 1000 毫秒后执行。for
循环是同步的,它会很快执行完毕。当循环结束时,i
的值变成了4
。- 由于
var
是函数作用域(在这里是全局作用域),所有的setTimeout
回调函数共享同一个i
变量。 - 当 1000 毫秒后回调函数开始执行时,它们去访问
i
,此时i
的值已经是4
了,所以它们都打印出4
。
如何解决?
(1) 使用立即执行函数表达式 (IIFE) 创建新作用域
for (var i = 1; i <= 3; i++) {(function(j) { // 使用 IIFE 创建一个新的函数作用域setTimeout(function() {console.log(j); // 输出 1, 2, 3}, 1000);})(i); // 将当前的 i 作为参数传入 IIFE
}
解析:每次循环,我们都创建一个立即执行的函数,并将当前的 i
值作为参数 j
传入。由于函数作用域的特性,每个 setTimeout
的回调函数都形成了一个闭包,它们各自“记住”了自己被创建时传入的 j
值。
(2) 使用 let
声明变量 (ES6 推荐)
ES6 的 let
提供了块级作用域,完美地解决了这个问题。
for (let i = 1; i <= 3; i++) { // 使用 letsetTimeout(function() {console.log(i); // 输出 1, 2, 3}, 1000);
}
解析:let
在每次循环迭代时都会为 i
创建一个新的绑定,并且每个 setTimeout
回调函数都会捕获(形成闭包)当前迭代的 i
值。这比 IIFE 的方式更简洁、更直观。
五、总结
闭包是 JavaScript 中一个强大而基础的概念,理解它对于深入掌握这门语言至关重要。让我们回顾一下本篇的核心要点:
- 定义:闭包是函数及其词法环境的组合,使得内部函数能“记住”并访问其外部函数作用域中的变量,即使外部函数已执行完毕。
- 原理:基于词法作用域和作用域链,当内部函数引用外部变量时,外部作用域会保留在内存中。
- 形成条件:通常需要函数嵌套、内部函数引用外部变量,并且内部函数在外部函数执行后仍然可访问。
- 常见应用:
- 封装私有变量:实现数据隐藏和模块化。
- 函数工厂:创建具有特定功能的函数。
- 回调与异步:在异步操作中保持状态。
- 注意事项:
- 内存消耗:可能导致内存占用增加,需注意释放。
- 循环陷阱:使用
var
时需特别小心,推荐使用let
或 IIFE 解决。
闭包可能初看时会觉得有些绕,但通过多看、多写、多实践,你会发现它无处不在,并且是你编写高效、优雅 JavaScript 代码的得力助手。在下一篇文章中,我们将继续探讨闭包的更多高级应用和潜在问题,敬请期待!