我从来不理解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.hqwc.cn/news/276513.html

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

相关文章

双端队列和优先级队列

文章目录 前言dequedeque底层设计迭代器设计 priority仿函数数组中的第k个最大元素优先级队列模拟实现pushpop调整仿函数存储自定义类型 前言 今天要介绍比较特殊的结构,双端队列。 还有一个适配器,优先级队列。 deque 栈的默认容器用了一个deque的东西…

react经验7:高亮关键字

预期效果: 实现原理 将需要高亮的关键词做成正则表达式 new RegExp((${word}), "gi")使用上述正则表达式切割目标字符串 origin.split(new RegExp((${word}), "gi"))切割结果会包含正则匹配到的词 过滤掉空字符,并对关键词包裹…

力扣 | 437. 路径总和 III

437. 路径总和 III mport java.util.ArrayList; import java.util.List;/*** int的取值范围&#xff1a;* -2^31 ~ 2^31-1* <p>* -2147483648 ~ 2147483647&#xff08;约等于10的9次方&#xff09;* <p>* long long的取值范围&#xff1a;* -2^63 ~ (2^63-1&…

Arduino uno循迹小车总结

1.HW-096 4路循迹模块&#xff08;红外发射器和红外接收器&#xff09; 输出信号&#xff1a;TTL电平&#xff08;可直接连接单片机I/0号&#xff0c;感应到传感器反射回来的红外光时,红指示灯亮&#xff0c;输出低电平&#xff1b;没有红外光时,指示灯不亮&#xff0c;输出高电…

契约锁解读:发生纠纷,法院如何判定电子合同真实有效性?

在签署电子合同时&#xff0c;大多数人最关心的依然是&#xff1a; 电子合同是否有法律效力&#xff1f; 电子合同是否可以作为司法证据&#xff1f; 如果发生纠纷&#xff0c;法院如何认证电子合同的有效性&#xff1f; 结合当前国家出台的相关法律法规&#xff1a;可靠的电子…

POST:http://XXX:XXXX/XXXX/XXXX(404 Not found)离谱

很离谱&#xff0c;同样的请求方式&#xff0c;不同的接口会有404的问题。看下边&#xff1a; 上边接口访问正常&#xff0c;下边接口出现404.且本地测试也可以&#xff0c;代码也推到公司git上了。真的很离谱。 我也不知道怎么回事&#xff0c;无语||||||| 哪位兄弟知道啊&a…

详解—C++右值引用

目录 一、右值引用概念 二、 左值与右值 三、引用与右值引用比较 四、值的形式返回对象的缺陷 五、移动语义 六、右值引用引用左值 七、完美转发 八、右值引用作用 一、右值引用概念 C98中提出了引用的概念&#xff0c;引用即别名&#xff0c;引用变量与其引用实体公共…

WX小程序案例(一):弹幕列表

WXML内容 <!--pages/formCase/formCase.wxml--> <!-- <text>pages/formCase/formCase.wxml</text> --> <view class"bk bkimg"><!-- <image src"/static/imgs/ceeb653ely1g9na2k0k6ug206o06oaa8.gif" mode"scal…

分享10个国内免费的AI绘画工具

谈到 AI 绘画&#xff0c;许多人会联想到 Midjourney、Stable Diffusion、DALLE2 等国外的知名绘画工具。 然而&#xff0c;这些国外的 AI 绘画工具大部分都是付费的&#xff0c;并且需要借助科学上网才能使用。这两个条件让许多人望而却步。 考虑到很多人无法进行科学上网&a…

【Pandas案例1】 根据某些相同属性列合并同类数据

文章目录 根据相同属性合并pandas行代码数据加载自定义方法主函数完整代码如下 根据相同属性合并pandas行 代码 提供的代码可直接运行 完整的逐步运行的ipynb代码项目化的py文件代码 以如下表格数据为例&#xff0c;针对t, i, j相同的行&#xff0c;对其后的v属性数据实现相加…

系统规划与管理师和信息系统项目管理师哪个好考?

软考系统规划与管理师和信息系统项目管理师是软考中备受关注的两个证书。这两个证书的相关知识领域广泛&#xff0c;对于从事IT行业的人们来说&#xff0c;都具有相当的吸引力。那么&#xff0c;对于考生而言&#xff0c;究竟哪个证书更适合呢&#xff1f;接下来&#xff0c;我…

Java 线程的基本概念

创建和运行线程 方法一&#xff0c;直接使用 Thread // 创建线程对象 Thread t new Thread() {public void run() {// 要执行的任务}};// 启动线程 t.start();例如&#xff1a; // 构造方法的参数是给线程指定名字&#xff0c;推荐 Thread t1 new Thread("t1") …