我从来不理解JavaScript闭包,但我用了它好多年

前言

 📫 大家好,我是南木元元,热衷分享有趣实用的文章,希望大家多多支持,一起进步!

 🍅 个人主页:南木元元

你是否学习了很久JavaScript但还没有搞懂闭包呢?今天就来聊一下被很多人誉为JavaScript中最难理解的概念之一的闭包。


目录

闭包的概念

闭包产生的原因

作用域&作用域链

闭包的本质

闭包的表现形式

闭包的用途

封装私有变量

做缓存

闭包的缺点

结语


闭包的概念

  • 红宝书(P309)上对于闭包的定义

闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。

  • MDN对闭包的定义

闭包是指那些能够访问自由变量的函数。其中自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

总结一下就是,闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

下面就是一个闭包的例子。

// 外部函数
function outerFunction() {let outerVariable = 'outer';// 内部函数function innerFunction() {console.log(outerVariable);}return innerFunction;
}const innerFunc = outerFunction();
innerFunc(); // outer

在上面的代码示例中,函数outerFunction内部有一个innerFunction函数,innerFunction函数可以访问到outerFunction函数中的变量,此时函数innerFunction就是一个闭包。

闭包产生的原因

作用域&作用域链

首先需要知道作用域和作用域链的概念。

作用域就是变量与函数的可访问范围

在js中,有三种作用域:

  • 全局作用域:变量在整个全局中都能被访问到
  • 函数作用域:变量只能在当前函数内被访问到
  • 块级作用域:变量通过ES6中的let和const来声明,只能在⼀对花括号{ }包裹的块中访问

作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找,这种层级关系就是作用域链。

  • 静态作用域

js 采用的是静态作用域词法作用域),即函数的作用域在函数定义时就确定了

var num = 10;
function f1(){console.log(num)
}
function f2(){var num  = 20;f1()
}
f2();//10

以上代码的执行结果为10,这段代码经历了这样的执行过程:

  • f2函数调用,f1函数调用
  • 在f1函数作用域内查找是否有局部变量num
  • 发现没找到,于是根据书写位置,向上一层作用域(全局作用域)查找,输出10

静态作用域也称为词法作用域,即在词法分析时生成的作用域,词法分析阶段,也可以理解为代码书写阶段,当你把函数书写到某个位置,不用执行,它的作用域就已经确定了。与之相对的是动态作用域,函数的作⽤域在函数调⽤时才确定,如果采用动态作用域,那么上述结果为20(如果想深入了解,可以去看这篇文章)。

在了解了js的作用域和作用域链后,让我们来看看下面这段代码:

var num = 10;function fn() {var num = 20;function fun() {console.log(num);//20}return fun;
}
var x = fn();
x();

上述例子中有三个作用域:全局作用域、fn的函数作用域、fun的函数作用域,它们的关系如下:

作用域链关系如下:

在这段代码中,fn的作用域指向有全局作用域和它本身,而fun的作用域指向全局作用域、fn和它本身。而作用域是从最底层向上找,当我们试图在fun这个函数里访问变量num的时候,此时函数作用域内没有num变量,当前作用域找不到,我们需要去上层作用域(fn函数作用域)找,在这里我们找到了num为20,输出即可(如果找到全局作用域还没有的话就会报错)。

闭包的本质

问大家一个问题:那是不是只有像上述例子一样返回函数才算是产生了闭包呢?

其实,闭包产生的本质就是:当前环境中存在指向父级作用域的引用。因此我们还可以这么做:

var fun;
function fn() {var num = 2;fun = function() {console.log(num); //2}
}
fn();
fun();

让fn执行,给fun赋值后,等于说现在fun拥有了全局、fn和fun本身这几个作用域的访问权限,还是自底向上查找,最近是在fn中找到了num,因此输出2。

在这里是外面的变量fun存在着父级作用域的引用,因此产生了闭包,形式变了,本质没有改变。

闭包的表现形式

明白了本质后,那我们思考下,实际场景中,闭包是如何体现的呢?

  • 返回一个函数(上面已经举例)
  • 作为函数参数传递
var a = 1;
function foo(){var a = 2;function baz(){console.log(a);}bar(baz);
}
function bar(fn){// 这就是闭包fn();
}
// 输出2,而不是1
foo();
  • 定时器、事件监听或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。

比如以下的闭包保存的仅仅是window和当前作用域。

// 定时器
setTimeout(function timeHandler(){console.log('111');
},100)// 事件监听
$('#btn').click(function(){console.log('222');
})
  • IIFE(立即执行函数表达式)创建闭包,保存了全局作用域window和当前的函数作用域,因此可以使用全局的变量。
var a = 2;
(function IIFE(){// 输出2console.log(a);
})();

现在,你是否会感叹一句:好家伙,原来我用了闭包这么多年!

闭包的用途

闭包有两个常用的用途:

  • 封装私有变量
  • 做缓存

封装私有变量

闭包可以使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量,以防止其被外部访问和修改。

在下面这个例子中,调用函数,输出的结果都是1,但是我们的代码效果是想让count每次加一。

function add() {let count = 0;count++;console.log(count);
}
add()   //输出1
add()   //输出1
add()   //输出1

一种显而易见的方法是将count提到函数体外,作为全局变量。这么做当然是可以解决问题,但是在实际开发中,一个项目由多人共同开发,你不清楚别人定义的变量名称是什么,很容易冲突,有什么其他的办法可以解决这个问题呢?

function add(){let count = 0function a(){count++console.log(count);}return a
}
var res = add() 
res() //1 
res() //2
res() //3

答案是用闭包。在上面的代码示例中,add函数返回了一个闭包a,其中包含了count变量。由于count只在add函数内部定义,因此外部无法直接访问它。但是,由于a函数引用了count变量,因此count变量的值可以在闭包内部被修改和访问。这种方式可以用于封装一些私有的数据和逻辑。

做缓存

函数一旦被执行完毕,其内存就会被销毁。而闭包可以使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

function foo(){var myName ='张三';let test = 1;var innerBar={getName: function(){console.log(test);return myName;},setName:function(newName){myName = newName;}}return innerBar;
}
var bar = foo();
console.log(bar.getName()); //1 张三
bar.setName('李四');
console.log(bar.getName()); //1 李四

这里var bar = foo() 执行完后本来应该被销毁,但是因为形成了闭包,所以导致foo执行上下文没有被销毁干净,被引用了的变量myName、test没被销毁,闭包里存放的就是变量myName、test,这个闭包就像是setName、getName的专属背包,setName、getName依然可以使用foo执行上下文中的test和myName。

闭包的应用是非常广泛的,比如常见的防抖和节流等其实也都是闭包的应用。

闭包的缺点

闭包也存在着一个潜在的问题,由于闭包会引用外部函数的变量,但是这些变量在外部函数执行完毕后没有被释放,那么这些变量会一直存在于内存中,这可能会带来内存泄漏问题,因此,需要及时释放闭包,即手动调用闭包函数,并将其返回值赋值为null,这样可以让闭包中的变量及时被垃圾回收器回收。

结语

本文主要介绍了被誉为JavaScript中最难理解的概念之一的闭包,闭包的表现形式多样、应用广泛,日常开发中其实都有闭包的身影,在实际的开发过程中,合理地使用闭包可以帮助我们更加高效地编写代码,提高程序的性能和可维护性。

🔥如果此文对你有帮助的话,欢迎💗关注、👍点赞、⭐收藏✍️评论支持一下博主~ 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/590700.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot解决前后端分离跨域问题:状态码403拒绝访问

最近在写和同学一起做一个前后端分离的项目,今日开始对接口准备进行 登录注册 的时候发现前端在发起请求后,抓包发现后端返回了一个403的错误,解决了很久发现是【跨域问题】,第一次遇到,便作此记录✍ 异常描述 在后端…

Java---网络编程

文章目录 1. 网络编程概述2. InetAddress3. 端口和协议4. Java网络API5. URL6. URLConnection类 1. 网络编程概述 1. 计算机网络:是指将地理位置不同的具有独立功能的多台计算机及其外部设备,通过通信线路连接起来,在网络操作系统、网络管理软…

2024年Mac专用投屏工具AirServer 7 .27 for Mac中文版

AirServer 7 .27 for Mac中文免费激活版是一款Mac专用投屏工具,能够通过本地网络将音频、照片、视频以及支持AirPlay功能的第三方App,从 iOS 设备无线传送到 Mac 电脑的屏幕上,把Mac变成一个AirPlay终端的实用工具。 目前最新的AirServer 7.2…

Matlab技巧[绘画逻辑分析仪产生的数据]

绘画逻辑分析仪产生的数据 逻分上抓到了ADC数字信号,一共是10Bit,12MHZ的波形: 这里用并口协议已经解析出数据: 导出csv表格数据(这个数据为补码,所以要做数据转换): 现在要把这个数据绘制成波形,用Python和表格直接绘制速度太慢了,转了一圈发现MATLAB很好用,操作方法如下:…

Bag-of-Words(BoW)

Bag-of-Words(BoW)模型是一种用于自然语言处理(NLP)的基本文本表示方法。它的核心思想是将文本数据转化为一个"词袋",忽略文本中词语的顺序和语法,只关注词汇的出现与否。BoW模型通常包括以下步骤…

逗号表达式与赋值表达式

逗号表达式和赋值表达式是C语言中常用的表达式类型。它们可以用于各种目的,包括计算和评估表达式、初始化变量、为函数调用提供参数以及将值分配给变量。 逗号表达式 逗号表达式允许在单个语句中计算和评估多个表达式。逗号分隔每个表达式,并且表达式从…

力扣134. 加油站

迭代 思路: 暴力模拟迭代;假设从第 idx 个加油站开始,使用一个变量对行驶的加油站个数计数,如果最后行驶的个数为 size,则是可行的;否则,行驶过的加油站都不可行;(加快更…

TypeScript 的基础语法

书接上上文:关于vue3的知识点 和 上文 :TypeScript的安装与报错 我们来接着看TypeScript 的基础语法 TypeScript 语法 1. 类型注解 类型注解是 变量后面约定类型的语法,用来约定类型,明确提示 // 约定变量 age 的类型为 numbe…

Ubuntu Linux 入门指南:面向初学者

目录 1. Ubuntu Linux 简介 Ubuntu 的由来 Ubuntu 与其他 Linux 发行版的比较 Debian: Fedora: openSUSE: Arch Linux: Linux Mint: 第二部分:安装 Ubuntu 1. 准备安装 系统需求 创建 Ubuntu 启…

Spring Cloud Gateway + Nacos 灰度发布

前言 本文将会使用 SpringCloud Gateway 网关组件配合 Nacos 实现灰度发布&#xff08;金丝雀发布&#xff09; 环境搭建 创建子模块服务提供者 provider&#xff0c;网关模块 gateway 父项目 pom.xml 配置 <?xml version"1.0" encoding"UTF-8"?…

数据库-期末考前复习-第3章-关系数据库标准语言SQL

1、掌握SQL语言实现数据查询、定义、操纵、控制的关键字。 数据查询&#xff1a;使用SELECT关键字进行数据查询操作。数据定义&#xff1a;使用CREATE和ALTER关键字进行数据库、表、视图、索引等的定义操作。数据操纵&#xff1a;使用INSERT、DELETE和UPDATE关键字进行数据的插…

2312d,d调用中文C++库

调用方: import core.stdcpp.string; //用C的串. import std.stdio;extern(C){bool bb(ref string a); } bool cc(ref string a);void main() {string c"bb";string d"";writeln(c,d);bool acc(c);writeln(c,d);bool bbb(d);writeln(a,b); }C这边,根据需要…

阿里云服务器开放端口Oracle 1521方法教程

阿里云服务器ECS端口是在安全组设置的&#xff0c;Oracle数据库1521端口号开放是在安全组中添加规则来实现的&#xff0c;阿里云服务器网aliyunfuwuqi.com来详细说下阿里云服务器开放Oracle 1521端口方法教程&#xff1a; 阿里云服务器开放Oracle 1521端口 在阿里云服务器ECS…

微信小程序自定义步骤条效果

微信小程序自定义一个步骤条组件&#xff0c;自定义文字在下面&#xff0c;已完成和未完成和当前进度都不一样的样式&#xff0c;可点击上一步和下一步切换流程状态&#xff0c;效果如下。 这是视频效果&#xff1a; 前端实现步骤条效果 下面我们一步步实现编码&#xff0c;自定…

华为鸿蒙运行Hello World

前言&#xff1a; 从11月中旬开始通过B站帝心接触鸿蒙&#xff0c;至今一个半月左右不到&#xff0c;从小白到入坑&#xff0c;再到看官网案例&#xff0c;分析案例&#xff0c;了解技术点&#xff0c;还需要理清思路&#xff0c;再写博客&#xff0c;在决定写 &#xff1c;Har…

配置Docker私有仓库

# 打开要修改的文件 vi /etc/docker/daemon.json # 添加内容&#xff1a; "insecure-registries":["http://自己服务器的ip地址:设置的端口号"] # 重加载 systemctl daemon-reload # 重启docker systemctl restart docker在自己设定的文件夹内使用DockerCo…

仓库管理系统

基于SSM框架的仓库管理系统

.net8 AOT编绎-跨平台调用C#类库的新方法-函数导出

VB.NET AOT无法编绎DLL,微软的无能&#xff0c;正是你的机会 .net8 AOT编绎-跨平台调用C#类库的新方法-函数导出 1&#xff0c;C#命令行创建工程&#xff1a;dotnet new classlib -o CSharpDllExport 2&#xff0c;编写一个静态方法&#xff0c;并且为它打上UnmanagedCallersO…

PWM应用篇

一.什么是PWM 用图话&#xff0c;如下图所示&#xff1a; PWM&#xff08;脉冲宽度调制&#xff09;&#xff1a;这是一种模拟控制方式&#xff0c;可以根据载荷的变化来调制晶体管基极或MOS管栅极的偏置&#xff0c;实现晶体管或MOS管导通时间的改变&#xff0c;从而改变开关稳…

【STM32】STM32学习笔记-TIM输入捕获(17)

00. 目录 文章目录 00. 目录01. 输入捕获简介02. 频率测量03. 输入捕获通道04. 主从触发模式05. 输入捕获基本结构06. PWMI基本结构07. 其它08. 附录 01. 输入捕获简介 IC&#xff08;Input Capture&#xff09;输入捕获 输入捕获模式下&#xff0c;当通道输入引脚出现指定电平…