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

news2024/12/23 6:20:16

前言

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

 🍅 个人主页:南木元元

你是否学习了很久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(){
  // 输出2
  console.log(a);
})();

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

闭包的用途

闭包有两个常用的用途:

  • 封装私有变量
  • 做缓存

封装私有变量

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

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

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

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

function add(){
    let count = 0
    function 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.coloradmin.cn/o/1302005.html

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

相关文章

精准捕捉异常时刻——从写好事件标题与内容开始

写在前面 在讨论如何写好监控器配置时的事件通知内容之前,需要明确这样一个逻辑: 当监控器检测规则生效后,它会针对系统业务数据进行一系列聚合数据处理并以事件的形式留存。这些事件记录可以理解为当前监控器检测对象发出的异常信号的承载…

WPS Office JS宏实现Excel转换为JSON格式数据

通过Excel JS宏,将表格中的数据以”列“形式导出为JSON数据格式。 我们在整理文档时,产品会通过Excel将功能点和功能描述分层级整理出来,有时需要将此数据导入到系统中,Web端对Json这类数据比较友好,所以这里试着使用E…

实现加盐加密方法以及MappedByteBuffer,RandomAccess

目录 自己实现 Spring Security MappedByteBuffer RandomAccess 加盐加密的实现 自己实现 传统MD5可通过彩虹表暴力破解, 加盐加密算法是一种常用的密码保护方法,它将一个随机字符串(盐)添加到原始密码中,然后再进…

Java面试题(每天10题)-------连载(46)

目录 Dubbo篇 1、Dubbo的默认集群容错方案 2、Dubbo支持哪些序列化方式? 3、Dubbo超时时间怎样设置? 4、服务调用超时问题怎么解决? 5、Dubbo在安全机制方面是如何解决的? 6、Dubbo和Dubbox之间的区别 7、Dubbo和Spring C…

java--正则表达式用于查找信息、用于搜索替换、分割内容

1.案例:使用正则表达式查找一段文本中的内容 需求:请把下面文本中的电话,邮箱,座机号码,热线都爬取出来。 2.正则表达式用于搜索替换、分割内容,需要结合String提供的如下方法完成

MySQL使用教程

数据构成了我们日益数字化的社会基础。想象一下,从移动应用和银行系统到搜索引擎,再到如 ChatGPT 这样的先进人工智能聊天机器人,这些工具若没有数据支撑,将寸步难行。你有没有好奇过这些海量数据都存放在哪里呢?答案正…

Java程序编写(下)

Scanner对象 通过java.util.Scanner来获得Scanner类 基本语法: Scanner s new Scanner(System.in)通过next()和nextLine()方法获取输入的字符串。 通过hasNext()和hasNextLine()判断是否还有输入的数据。 next(): 1、一定要读取到有效字符后才可以结束输入 2、…

uniapp移动端悬浮按钮(吸附边缘)

Uniapp移动端悬浮按钮可以通过CSS实现吸附边缘的效果。具体实现步骤如下&#xff1a; html&#xff1a; <movable-area class"movable-area"><movable-view class"movable-view" :position"position" :x"x" :y"y"…

列出最佳Aspera替代方案,给你执掌数据传输的权

Aspera是一种高速文件传输技术&#xff0c;广泛应用于科学研究、医疗保健、媒体和娱乐等领域。然而&#xff0c;由于Aspera的高昂价格&#xff0c;很多人不得不寻找替代方案。本文将列举最佳Aspera替代方案&#xff0c;以便执掌数据传输的权利。 1、FileZilla FileZilla是一款…

XUbuntu22.04之8款免费UML工具(一百九十七)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

基于ssm的电动车租赁网站论文

摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本电动车租赁网站就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

FreeSSL申请免费域名证书

本文详细讲解如何申请免费证书&#xff0c;需要先准备好域名&#xff0c;将服务器IP和域名绑定。 1、注册FreeSSL账号 网址&#xff1a; https://freessl.org/ 2、申请流程 登录后首页输入域名&#xff0c;然后点击Create certificate&#xff0c;跳转到证书申请页面。 或者…

LLM中的Prompt提示

简介 在LLM中&#xff0c;prompt&#xff08;提示&#xff09;是一个预先设定的条件&#xff0c;它可以限制模型自由发散&#xff0c;而是围绕提示内容进行展开。输入中添加prompt&#xff0c;可以强制模型关注特定的信息&#xff0c;从而提高模型在特定任务上的表现。 结构 …

Ubuntu安装TensorRT

文章目录 1. 安装CUDAa. 下载CUDAb. 安装CUDAc. 验证CUDA 2. 安装CUDNNa. 下载CUDNNb. 安装CUDNNc. 验证CUDNN 3. 安装TensorRTa. 下载TensorRTb. 解压TensorRTc. 安装TensorRTd. 安装uff和graphsurgeone. 验证是否安装成功f. 备注 关注公众号&#xff1a;『AI学习星球』 回复&…

kafka支持外网访问

kafka支持外网访问 1.kafka正常部署之后如果不修改&#xff0c;外网是无法访问的&#xff0c;具体如下&#xff08;这里是单节点&#xff09; 2.这个时候需要修改kafka的config中的server.properties中的 listeners 修改为0.0.0.0 监控所有网卡&#xff0c;advertised.listene…

硬件开发笔记(十五):RK3568底板电路VGA显示接口原理图分析

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/134849296 红胖子网络科技博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软硬…

tp5 rewrite nginx重写

tp框架,默认的访问路径是 www.xxxx.com/index.php/admin/shop/index格式的&#xff0c;为了方便和更规范&#xff0c;也看起来有逼格一些&#xff0c;需要将index.php去掉 无index.php就会报404 我这里是宝塔 #地址重写if (!-e $request_filename) {rewrite ^(.*)$ /index.…

解决火狐浏览器拖拽事件打开新页面的问题

产生原因及解决方案 我们在进行拖拽事件的编写时会发现&#xff0c;在火狐浏览器上会发生打开新窗口的问题&#xff0c;这是火狐浏览器的一个特性。 这是因为在 Firefox 中 ondrop 事件会触发 Firefox 自带的拖拽搜索功能&#xff0c;在 ondrop 事件触发执行时触发的函数中加…

【深度挖掘Java性能调优】「底层技术原理体系」深入探索Java服务器性能监控Metrics框架的实现原理分析(Counter篇)

目录 前提概要监控工作可以分为四个部分 监控开发任务Metrics中的基础数据类型计数器&#xff08;Counter&#xff09;统计 API 访问中异常&#xff08;1000/1500&#xff09;的次数统计 API 的调用量统计特定事件发生的次数Counter的底层原理基础 (Base) 计数器单元 (Cell) 数…

MySQL行锁范围分析(行锁、间隙锁、临键锁)

MySQL 中锁的概念 排它锁&#xff08;Exclusive Lock&#xff09; X 锁&#xff0c;也称为写锁&#xff0c;若事务T对对象A加上X锁&#xff0c;则只允许T读取和修改A&#xff0c;其他任何事物都不能再对A 加任何锁&#xff0c;直到T释放A上的锁。 SELECT…FOR UPDATE 对读取的…