js知识点之闭包
闭包
什么是闭包
闭包,是 JavaScript 中一个非常重要的知识点,也是我们前端面试中较高几率被问到的知识点之一。
打开《JavaScript 高级程序设计》和《 JavaScript 权威指南》,会发现里面针对闭包的解释各执一词,在网络上搜索关于闭包的内容,也发现众说纷纭,这就导致了这个知识点本身显得有点神秘,甚至还有一点玄幻。
那么这个知识点真的有那么深奥么?
非也!其实要理解 JavaScript 中的闭包,非常容易,但是在此之前你需要先知道以下两个知识点:
- JavaScript 中的作用域和作用域链
- JavaScript 中的垃圾回收
这里我们来简单回顾一下这两个知识点:
1. JavaScript 中的作用域和作用域链
- 作用域就是一个独立的地盘,让变量不会外泄、暴露出去,不同作用域下同名变量不会有冲突。
- 作用域在定义时就确定,并且不会改变。
- 如果在当前作用域中没有查到值,就会向上级作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。
2. JavaScript 中的垃圾回收
- Javascript 执行环境会负责管理代码执行过程中使用的内存,其中就涉及到一个垃圾回收机制
- 垃圾收集器会定期(周期性)找出那些不再继续使用的变量,只要该变量不再使用了,就会被垃圾收集器回收,然后释放其内存。如果该变量还在使用,那么就不会被回收。
OK,有了这 2 个知识点的铺垫后,接下来我们再来看什么是闭包。
闭包不是一个具体的技术,而是一种现象,是指在定义函数时,周围环境中的信息可以在函数中使用。换句话说,执行函数时,只要在函数中使用了外部的数据,就创建了闭包。
而作用域链,正是实现闭包的手段。
什么?只要在函数中使用了外部的数据,就创建了闭包?
真的是这样么?下面我们可以证明一下:

在上面的代码中,我们在函数 a 中定义了一个变量 i,然后打印这个 i 变量。对于 a 这个函数来讲,自己的函数作用域中存在 i 这个变量,所以我们在调试时可以看到 Local 中存在变量 i。
下面我们将上面的代码稍作修改,如下图:

在上面的代码中,我们将声明 i 这个变量的动作放到了 a 函数外面,也就是说 a 函数在自己的作用域已经找不到这个 i 变量了,它会怎么办?
学习了作用域链的你肯定知道,它会顺着作用域链一层一层往外找。然而上面在介绍闭包时说过,如果出现了这种情况,也就是函数使用了外部的数据的情况,就会创建闭包。
仔细观察调试区域,我们会发现此时的 i 就放在 Closure 里面的,从而证实了我们前面的说法。
那么是一个函数下所有的变量声明都会被放入到闭包这个封闭的空间里面么?
倒也不是,放不放入到闭包中,要看其他地方有没有对这个变量进行引用,例如:
在上面的代码中,函数 c 中一个变量都没有创建,却要打印 i、j、k 和 x,这些变量分别存在于 a、b 函数以及全局作用域中,因此创建了 3 个闭包,全局闭包里面存储了 i 的值,闭包 a 中存储了变量 j 和 k 的值,闭包 b 中存储了变量 x 的值。
但是你仔细观察,你就会发现函数 b 中的 y 变量并没有被放在闭包中,所以要不要放入闭包取决于该变量有没有被引用。
当然,此时的你可能会有这样的一个新问题,那么多闭包,那岂不是占用内存空间么?
实际上,如果是自动形成的闭包,是会被销毁掉的。例如:
在上面的代码中,我们在第 16 行尝试打印输出变量 k,显然这个时候是会报错的,在第 16 行打一个断点调试就可以清楚的看到,此时已经没有任何闭包存在,垃圾回收器会自动回收没有引用的变量,不会有任何内存占用的情况。
当然,这里我指的是自动产生闭包的情况,关于闭包,有时我们需要根据需求手动的来制造一个闭包。
来看下面的例子:
function eat(){var food = "鸡翅";console.log(food);
}
eat(); // 鸡翅
console.log(food); // 报错
在上面的例子中,我们声明了一个名为 eat 的函数,并对它进行调用。
JavaScript 引擎会创建一个 eat 函数的执行上下文,其中声明 food 变量并赋值。
当该方法执行完后,上下文被销毁,food 变量也会跟着消失。这是因为 food 变量属于 eat 函数的局部变量,它作用于 eat 函数中,会随着 eat 的执行上下文创建而创建,销毁而销毁。所以当我们再次打印 food 变量时,就会报错,告诉我们该变量不存在。
但是我们将此代码稍作修改:
function eat(){var food = '鸡翅';return function(){console.log(food);}
}
var look = eat();
look(); // 鸡翅
look(); // 鸡翅
在这个例子中,eat 函数返回一个函数,并在这个内部函数中访问 food 这个局部变量。调用 eat 函数并将结果赋给 look 变量,这个 look 指向了 eat 函数中的内部函数,然后调用它,最终输出 food 的值。
为什么能访问到 food,原因很简单,上面我们说过,垃圾回收器只会回收没有被引用到的变量,但是一旦一个变量还被引用着的,垃圾回收器就不会回收此变量。在上面的示例中,照理说 eat 调用完毕 food 就应该被销毁掉,但是我们向外部返回了 eat 内部的匿名函数,而这个匿名函数有引用了 food,所以垃圾回收器是不会对其进行回收的,这也是为什么在外面调用这个匿名函数时,仍然能够打印出 food 变量的值。
至此,闭包的一个优点或者特点也就体现出来了,那就是:
- 通过闭包可以让外部环境访问到函数内部的局部变量。
- 通过闭包可以让局部变量持续保存下来,不随着它的上下文环境一起销毁。
通过此特性,我们可以解决一个全局变量污染的问题。早期在 JavaScript 还无法进行模块化的时候,在多人协作时,如果定义过多的全局变量 有可能造成全局变量命名冲突,使用闭包来解决功能对变量的调用将变量写到一个独立的空间里面,从而能够一定程度上解决全局变量污染的问题。
例如:
var name = "GlobalName";
// 全局变量
var init = (function () {var name = "initName";function callName() {console.log(name);// 打印 name}return function () {callName();// 形成接口}
}());
init(); // initName
var initSuper = (function () {var name = "initSuperName";function callName() {console.log(name);// 打印 name}return function () {callName();// 形成接口}
}());
initSuper(); // initSuperName
好了,在此小节的最后,我们来对闭包做一个小小的总结:
-
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
-
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
-
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
闭包经典问题
聊完了闭包,接下来我们来看一个闭包的经典问题。
for (var i = 1; i <= 3; i++) {setTimeout(function () {console.log(i);}, 1000);
}
在上面的代码中,我们预期的结果是过 1 秒后分别输出 i 变量的值为 1,2,3。但是,执行的结果是:4,4,4。
实际上,问题就出在闭包身上。你看,循环中的 setTimeout 访问了它的外部变量 i,形成闭包。
而 i 变量只有 1 个,所以循环 3 次的 setTimeout 中都访问的是同一个变量。循环到第 4 次,i 变量增加到 4,不满足循环条件,循环结束,代码执行完后上下文结束。但是,那 3 个 setTimeout 等 1 秒钟后才执行,由于闭包的原因,所以它们仍然能访问到变量 i,不过此时 i 变量值已经是 4 了。
要解决这个问题,我们可以让 setTimeout 中的匿名函数不再访问外部变量,而是访问自己内部的变量,如下:
for (var i = 1; i <= 3; i++) {(function (index) {setTimeout(function () {console.log(index);}, 1000);})(i)
}
这样 setTimeout 中就可以不用访问 for 循环声明的变量 i 了。而是采用调用函数传参的方式把变量 i 的值传给了 setTimeout,这样它们就不再创建闭包,因为在我自己的作用域里面能够找到 i 这个变量。
当然,解决这个问题还有个更简单的方法,就是使用 ES6 中的 let 关键字。
它声明的变量有块作用域,如果将它放在循环中,那么每次循环都会有一个新的变量 i,这样即使有闭包也没问题,因为每个闭包保存的都是不同的 i 变量,那么刚才的问题也就迎刃而解。
for (let i = 1; i <= 3; i++) {setTimeout(function () {console.log(i);}, 1000);
}
真题解答
- 闭包是什么?闭包的应用场景有哪些?怎么销毁闭包?
闭包是一个封闭的空间,里面存储了在其他地方会引用到的该作用域的值,在 JavaScript 中是通过作用域链来实现的闭包。
只要在函数中使用了外部的数据,就创建了闭包,这种情况下所创建的闭包,我们在编码时是不需要去关心的。
我们还可以通过一些手段手动创建闭包,从而让外部环境访问到函数内部的局部变量,让局部变量持续保存下来,不随着它的上下文环境一起销毁。
使用闭包可以解决一个全局变量污染的问题。
如果是自动产生的闭包,我们无需操心闭包的销毁,而如果是手动创建的闭包,可以把被引用的变量设置为 null,即手动清除变量,这样下次 JavaScript 垃圾回收器在进行垃圾回收时,发现此变量已经没有任何引用了,就会把设为 null 的量给回收了。
-EOF-
相关文章:

js知识点之闭包
闭包 什么是闭包 闭包,是 JavaScript 中一个非常重要的知识点,也是我们前端面试中较高几率被问到的知识点之一。 打开《JavaScript 高级程序设计》和《 JavaScript 权威指南》,会发现里面针对闭包的解释各执一词,在网络上搜索关…...

LORA微调,让大模型更平易近人
技术背景 最近和大模型一起爆火的,还有大模型的微调方法。 这类方法只用很少的数据,就能让大模型在原本表现没那么好的下游任务中“脱颖而出”,成为这个任务的专家。 而其中最火的大模型微调方法,又要属LoRA。 增加数据量和模…...
LabVIEW全自动样品处理系统有哪些优势?
基于LabVIEW的全自动样品处理系统在现代科研和工业应用中展现出显著的优势,其在数据采集、分析和控制方面的性能使其成为提高效率和精度的理想选择。以下是该系统的详细优势: 高效自动化 LabVIEW的图形化编程语言极大地简化了自动化流程的开发。用户可…...
shell脚本操作http请求的返回值——shell处理json格式数据
日常工作中,我们经常会遇到http请求会返回大量格式固定的数据,而我们只需要其中的一部分,那么怎么提取我们想要的字段呢。 这里会介绍一种用shell脚本处理http请求返回,或者处理json格式数据的方式。 这里我们用到了 jq这个强大的…...
leetcode力扣 300. 最长递增子序列 II
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 示例 1&#…...
C++_vector简单源码剖析:vector模拟实现
文章目录 🚀1.迭代器🚀2.构造函数与析构函数⚡️2.1 默认构造函数vector()⚡️2.2 vector(int n, const T& value T())⚡️内置类型也有构造函数 ⚡️2.3 赋值重载operator⚡️2.4 通用迭代器拷贝⚡️2.5 vector(initializer_list<T> il)⚡️…...

第3章 数据链路层
王道学习 考纲内容 (一)数据链路层的功能 (二)组帧 (三)差错控制 检错编码;纠错编码 (四)流量控制与可靠传输机制 流量控制、可靠传输与滑动窗口…...

使用OrangePi KunPeng Pro部署AI模型
目录 一、OrangePi Kunpeng Pro简介二、环境搭建三、模型运行环境搭建(1)下载Ollama用于启动并运行大型语言模型(2)配置ollama系统服务(3)启动ollama服务(4)启动ollama(5)查看ollama运行状态四、模型部署(1)部署1.8b的qwen(2)部署2b的gemma(3)部署3.8的phi3(4)部署4b的qwen(5)部…...

SpringMVC 数据映射VC
从 view 层发送请求到Controller,在Controller中获取参数: 在不输入值时会报400,参数错误 在不输入值时num默认为null 没有找到对应标签名称叫nums的,输入任何值时都报400 设置required默认值为false,即使表单没有nums…...

Clickhouse Bitmap 类型操作总结—— Clickhouse 基础篇(四)
文章目录 创建 Bitmap 对象Bitmap 转换为整数数组计算总数(去重)值指定start, end 索引生成子 Bitmap指定 start 索引和数量限制生成子 Bitmap指定偏移量生成子 Bitmap是否包含指定元素两个 Bitmap 是否存在相同元素一个是否为另一个 Bitmap 的子集求最小…...

202474读书笔记|《我自我的田渠归来》——愿你拥有向上的力量,一切的好事都应该有权利发生
202474读书笔记|《我自我的田渠归来》——愿你拥有向上的力量 《我自我的田渠归来》作者张晓风,被称为华语散文温柔的一支笔,她的短文很有味道,角度奇特,温柔慈悲而敏锐。 很幸运遇到了这本书,以她的感受重新认识一些事…...

SheetJS V0.17.5 导入 Excel 异常修复 Invalid HTML:could not find<table>
导入 Excel 提示错误:Invalid HTML:could not find<table> 检查源代码 发现 table 属性有回车符 Overview: https://docs.sheetjs.com/docs/ Source: https://git.sheetjs.com/sheetjs/sheetjs/issues The public-facing websites of SheetJS: sheetjs.com…...

重学java51.Collections集合工具类、泛型
"我已不在地坛,地坛在我" —— 《想念地坛》 24.5.28 一、Collections集合工具类 1.概述:集合工具类 2.特点: a.构造私有 b.方法都是静态的 3.使用:类名直接调用 4.方法: static <T> boolean addAll(collection<? super T>c,T... el…...

OSPF扩展知识2
FA-转发地址 正常 OSPF 区域收到的 5 类 LSA 不存在 FA 值; 产生 FA 的条件: 1、5类LSA ----假设 R2为 ASBR,90/0 口工作的 OSPF 中,g0/1 口工作在非 ospf 协议或不同 ospf 进程中;若 g0/1 也同时宣告在和 g0/0 相同的 OSPF 进程…...

数据库技术基础
数据库技术基础 导航 文章目录 数据库技术基础导航一、基础概念数据库系统数据库管理系统DBMS分类数据库技术的发展数据库体系结构 二、数据模型数据模型基本概念 三、数据库的控制功能事务概述SOL中事务定义语句日志文件故障种类两个操作Undo/Redo事务故障的恢复系统故障的恢…...

这些项目,我当初但凡参与一个,现在也不至于还是个程序员
10年前,我刚开始干开发不久,我觉得这真是一个有前景的职业,我觉得我的未来会无限广阔,我觉得再过几年,我一定工资不菲。于是我开始像很多大佬说的那样,开始制定职业规划,并且坚决执行。但过去这…...

ch2应用层--计算机网络期末复习
2.1应用层协议原理 网络应用程序位于应用层 开发网络应用程序: 写出能够在不同的端系统上通过网络彼此通信的程序 2.1.1网络应用程序体系结构分类: 客户机/服务器结构 服务器: 总是打开(always-on)具有固定的、众所周知的IP地址 主机群集常被用于创建强大的虚拟服务器 客…...

Red Hat Enterprise Linux (RHEL) 8.10 发布 - 红帽企业 Linux 8 完美终结版
Red Hat Enterprise Linux (RHEL) 8.10 (x86_64, aarch64) - 红帽企业 Linux 红帽企业 Linux 8 完美终结版 请访问原文链接:Red Hat Enterprise Linux (RHEL) 8.10 (x86_64, aarch64) - 红帽企业 Linux,查看最新版。原创作品,转载请保留出处…...

.NET 直连SAP HANA数据库
前言 上个项目碰到的需求,IT部门要求直连SAP的HANA数据库,以只读的权限读取SAP部门开发的CDS视图,是个有点复杂的工程,需要从成品一直往前追溯到原材料的产地,和交货单、工单、采购订单有相当程度上的关联 IT部门要求…...
HTML <from>表单
定义:<form>元素定义了一个表单,用户可以在表单中输入数据,这些数据可以被提交到服务器。 属性: action:指定表单提交时的目标URL(服务器端脚本的地址)。 method:定义提交表…...
谷歌浏览器插件
项目中有时候会用到插件 sync-cookie-extension1.0.0:开发环境同步测试 cookie 至 localhost,便于本地请求服务携带 cookie 参考地址:https://juejin.cn/post/7139354571712757767 里面有源码下载下来,加在到扩展即可使用FeHelp…...

装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...

【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...

从WWDC看苹果产品发展的规律
WWDC 是苹果公司一年一度面向全球开发者的盛会,其主题演讲展现了苹果在产品设计、技术路线、用户体验和生态系统构建上的核心理念与演进脉络。我们借助 ChatGPT Deep Research 工具,对过去十年 WWDC 主题演讲内容进行了系统化分析,形成了这份…...

Python爬虫(一):爬虫伪装
一、网站防爬机制概述 在当今互联网环境中,具有一定规模或盈利性质的网站几乎都实施了各种防爬措施。这些措施主要分为两大类: 身份验证机制:直接将未经授权的爬虫阻挡在外反爬技术体系:通过各种技术手段增加爬虫获取数据的难度…...

2025盘古石杯决赛【手机取证】
前言 第三届盘古石杯国际电子数据取证大赛决赛 最后一题没有解出来,实在找不到,希望有大佬教一下我。 还有就会议时间,我感觉不是图片时间,因为在电脑看到是其他时间用老会议系统开的会。 手机取证 1、分析鸿蒙手机检材&#x…...
Axios请求超时重发机制
Axios 超时重新请求实现方案 在 Axios 中实现超时重新请求可以通过以下几种方式: 1. 使用拦截器实现自动重试 import axios from axios;// 创建axios实例 const instance axios.create();// 设置超时时间 instance.defaults.timeout 5000;// 最大重试次数 cons…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

ArcGIS Pro制作水平横向图例+多级标注
今天介绍下载ArcGIS Pro中如何设置水平横向图例。 之前我们介绍了ArcGIS的横向图例制作:ArcGIS横向、多列图例、顺序重排、符号居中、批量更改图例符号等等(ArcGIS出图图例8大技巧),那这次我们看看ArcGIS Pro如何更加快捷的操作。…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...