当前位置: 首页 > news >正文

【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 陷阱:深入理解 letconst 的妙用
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,输出 "我是外部函数的变量"

在这个例子中:

  1. outerFunction 定义了 outerVariableinnerFunction
  2. innerFunction 访问了 outerVariable
  3. outerFunction 返回了 innerFunction 并赋值给 myClosure
  4. 尽管 outerFunction 已经执行完毕,但由于 myClosure (即 innerFunction) 仍然存在,并且它引用了 outerVariable,所以 outerVariable 并未被销毁。
  5. 当我们调用 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)

解析:

  1. makeAdder 是外部函数,它接收一个参数 x
  2. 它内部定义并返回了一个匿名函数,这个匿名函数接收一个参数 y
  3. 这个匿名函数(内部函数)引用了外部函数 makeAdder 的参数 x
  4. 当我们调用 makeAdder(5) 时,makeAdder 执行并返回了内部函数。此时,一个闭包形成了。这个闭包“记住”了 x 的值是 5。我们将这个闭包赋值给了 add5
  5. 同理,add10 是另一个闭包,它记住了 x 的值是 10
  6. 当我们调用 add5(2) 时,实际上是在执行那个记住了 x=5 的内部函数,并传入 y=2,所以结果是 5 + 2 = 7 5 + 2 = 7 5+2=7
  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)

解析:

  1. createCounter 函数内部定义了 privateCount
  2. 它返回了一个包含三个方法的对象:incrementdecrementgetValue
  3. 这三个方法都是在 createCounter 内部定义的,所以它们可以访问 privateCount,形成了闭包。
  4. 由于 privateCount 变量只存在于 createCounter 的作用域内,外部无法直接访问它,只能通过返回对象提供的公共方法来间接操作它。
  5. 每次调用 createCounter() 都会创建一个新的作用域和新的 privateCount,因此 counter1counter2 互不影响,拥有各自独立的计数器。

3.2 实现回调与异步

闭包在处理回调函数和异步操作(如 setTimeoutsetInterval、事件监听、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);
}

为什么会这样?

  1. setTimeout 是异步的,它会让内部的函数在约 1000 毫秒后执行。
  2. for 循环是同步的,它会很快执行完毕。当循环结束时,i 的值变成了 4
  3. 由于 var 是函数作用域(在这里是全局作用域),所有的 setTimeout 回调函数共享同一个 i 变量。
  4. 当 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 中一个强大而基础的概念,理解它对于深入掌握这门语言至关重要。让我们回顾一下本篇的核心要点:

  1. 定义:闭包是函数及其词法环境的组合,使得内部函数能“记住”并访问其外部函数作用域中的变量,即使外部函数已执行完毕。
  2. 原理:基于词法作用域和作用域链,当内部函数引用外部变量时,外部作用域会保留在内存中。
  3. 形成条件:通常需要函数嵌套、内部函数引用外部变量,并且内部函数在外部函数执行后仍然可访问。
  4. 常见应用
    • 封装私有变量:实现数据隐藏和模块化。
    • 函数工厂:创建具有特定功能的函数。
    • 回调与异步:在异步操作中保持状态。
  5. 注意事项
    • 内存消耗:可能导致内存占用增加,需注意释放。
    • 循环陷阱:使用 var 时需特别小心,推荐使用 let 或 IIFE 解决。

闭包可能初看时会觉得有些绕,但通过多看、多写、多实践,你会发现它无处不在,并且是你编写高效、优雅 JavaScript 代码的得力助手。在下一篇文章中,我们将继续探讨闭包的更多高级应用和潜在问题,敬请期待!


相关文章:

  • 【MySQL】实战时遇到的几个 tips
  • ​宠智灵AI诊疗助手:打造宠物医疗的“第二医生”与智能化引擎
  • MySQL--day6--单行函数
  • 机器人强化学习入门学习笔记(四)
  • React从基础入门到高级实战:React 基础入门 - 状态与事件处理
  • 聚焦 Microsoft Fabric,释放数据潜力
  • CAS详解
  • 第三章 软件工程模型和方法
  • 初识Flask框架
  • 直线导轨运转过程中如何避免震动发生?
  • 量子传感器:开启微观世界的精准探测
  • VSCode如何像Pycharm一样“““回车快速生成函数注释文档?如何设置文档的样式?autoDocstring如何设置自定义模板?
  • 3dczml时间动态图型场景
  • Linux里more 和 less的区别
  • 【自定义类型-联合和枚举】--联合体类型,联合体大小的计算,枚举类型,枚举类型的使用
  • 中国经济的结构性困境与制度性瓶颈:关键卡点深度解析
  • 撤销Conda初始化
  • PyTorch 中unsqueeze(-1)用法
  • 城市地下“隐形卫士”:激光甲烷传感器如何保障燃气安全?
  • 《Android 应用开发基础教程》——第十五章:Android 动画机制详解(属性动画、帧动画、过渡动画)
  • 怎么自己在家做网站/成都百度推广开户公司
  • 如何在国外网站做翻译兼职/网站排名费用
  • 移动互联网技术就业前景/郑州seo顾问
  • 李静做的化妆品网站/免费的推广平台
  • 深圳做兼职的网站/百度推广时间段在哪里设置
  • 用返利网站做爆款/免费下载百度软件