当前位置: 首页 > Web前端 > JavaScript

JS中关于闭包的一切

时间:2023-03-26 22:32:18 JavaScript

什么是闭包关于闭包的概念似乎众说纷纭:说法一:闭包是在另一个函数作用域内引用变量的函数,通常在嵌套函数中实现。——来自小红书;陈述2:函数和函数内部可以访问的变量(也叫环境)的总和是一个闭包——来自这篇文章;看一段代码:functionouter(){letn=1functioninner(){console.log(n)}returninner}根据第一条语句,函数inner()是一个闭包;根据第二条语句,变量n和函数inner()形成一个闭包,即函数outer()是一个闭包;其实这两种说法都是对的,只是具体说辞就不用赘述了。首先我们要搞清楚闭包的含义,闭包解决的是什么问题,再看看闭包的概念,水到渠成。闭包解决了什么问题?先看结论。闭包实现:读取函数内部的变量;将变量保存在内存中;读取函数内部的变量。首先明确一点,在JS中,函数内部可以访问函数外部的变量,反之则不行,即函数内部的变量不能从函数外部访问。这就涉及到JS作用域链的知识,下面会介绍。如果对于一些变量,出于安全的考虑不想直接暴露在外面,那么我可以把这个变量放在一个函数里面,然后暴露一个接口来访问这个变量。比如我有一个变量n需要隐藏,但是需要在外部累加,可以这样实现:letaddFunc=null//addFunc位于全局作用域functionouter(){letn=1//把变量n隐藏起来,不能从外部直接访问。functioninner(){//这也是一个间接访问n的接口console.log(n)}addFunc=function(){//一个间接操作n的接口,相当于settern++}returninner}letres=outer()res()//1addFunc()res()//2其中addFunc()方法是用于操作变量n的接口。其实上面代码中有两个闭包,因为函数inner和addFunc中使用了局部变量n,所以局部变量n和inner和addFunc分别形成了一个闭包。但是为什么n不能外积呢?由于变量n属于outer函数的作用域,当函数执行时(即执行letres=outer()),它的作用域会被清空,内存也会被回收,但是从两个res()根据打印出来的结果,n并没有被回收,因为闭包最重要的功能之一就是在内存中存储变量。将变量保存在内存中为什么闭包可以将变量保存在内存中?先了解一下JS的垃圾回收机制。不同于类C语言需要程序员手动清理内存,JS提供了自动垃圾回收机制。原理是每隔一段时间检查是否有需要回收的变量,判断的依据是该变量是否存在于当前执行上下文中,是否在其他地方被引用。代码中letres=outer()实际上是将函数inner赋值给了一个全局变量res,全局变量只有在程序退出或者网页关闭时才会被回收,而函数inner是指函数中的变量nouter,soouter的上下文不会随着调用结束而被回收。注意如果直接通过outer()()调用函数,不会形成闭包:outer()()//1addFunc()outer()()//1原因是:outer()()只是内部函数执行后,其上下文会在函数执行完毕后被回收。所以letres=outer()是必要的。闭包和作用域链(重要)知道了闭包是用来保存一些局部变量的,那么JS是如何将局部变量保存在内存中的呢?这就需要了解JS中的作用域链了。我们看一下红皮书(第四版10.14)中的例子://该函数用于比较两个对象的属性值functioncreateComparisonFunction(propertyName){//局部变量propertyNamereturnfunction(object1,object2){letvalue1=object1[propertyName]//propertyName在匿名函数中被引用letvalue2=object2[propertyName]if(value1value2){return1}else{return0}}}letcompare=createComparisonFunction('name')letresult=compare({name:'Nicholas'},{name:'Matt'})显然,局部变量propertyName和返回的匿名函数形成了一个闭包。先说结论:propertyName之所以会保存在内存中,是因为返回的匿名函数的作用域链包含了createComparisonFunction函数的作用域。通用函数的作用域链我们先来看看通用函数(非闭包)作用域链的形成。下面的函数:functioncompare(value1,value2){if(value1value2){return1}else{return0}}letresult=compare(5,10)several概念区分:上下文、变量对象、活动对象、作用域链执行上下文(简称“上下文”),包括变量和函数的上下文,决定了哪些数据可以被访问及其行为;每个上下文都有一个关联的变量对象,上下文中定义的所有变量和函数都在这个对象上,函数的上下文称为活动对象;当函数被调用时,它会创建自己的上下文,同时创建它的变量对象的作用域链。作用域链决定了在所有级别的上下文中访问变量和函数的顺序;当调用函数时,会创建函数的上下文和函数的作用域链。具体关系如下:看起来有点绕口,其实重点只在compare函数的作用域链上,它的作用域链有两个变量对象:全局变量对象和函数的活动对象.当函数被执行时,它的执行上下文将被销毁。由于函数的活动对象只依赖于它的上下文,它的活动对象也会被销毁,所以存放在活动对象中的变量会被回收。只要网页没有关闭,全局变量对象就会一直存在。这符合我们一贯的认知。闭包的作用域链那么什么是闭包形成的作用域链呢?看前面的例子,当执行createComparisonFunction函数时,也就是运行下面的代码时:letcompare=createComparisonFunction('name')letresult=compare({name:'Nicholas'},{name:'Matt'})outerfunctioncreateComparisonFunction和innerfunction形成的作用域链compare:好像比较规避,重点还是放在两个函数的作用域链上。可以看出,两个函数的作用域链是重叠的,即都可以访问外层函数createComparisonFunction的活动对象(局部变量propertyName),以及全局变量对象。这是与一般函数作用域链最大的区别。那么这个时候,我们可以回顾一下我们一开始提到的结论:propertyName之所以会存在内存中,是因为返回的匿名函数的作用域链包含了createComparisonFunction函数的作用域。createComparisonFunction函数执行时,其上下文的作用域链会被销毁,但由于其活动对象被匿名函数的作用域链引用,而匿名函数又被全局变量result引用,这种相互依赖导致了createComparisonFunction的函数的活动对象保留在内存中。为了让活动对象被回收,避免内存泄漏,需要手动设置result=null,让匿名函数失去对全局上下文的引用,让垃圾回收器释放它的内存。闭包和垃圾回收机制上面已经反复提到了JS的垃圾回收机制,那么垃圾回收机制是如何工作的呢?如何判断某块内存是否应该被回收?下面简单介绍一下JS的垃圾回收机制。像C这样的低级语言一般都有低级的内存管理接口,比如malloc()和free(),内存申请和垃圾回收都需要手动完成。相比之下,JavaScript在创建变量(对象、字符串等)时自动分配内存,在不使用时自动释放。JS中最常用的垃圾回收策略是“MarkSweep”。垃圾回收程序在运行时,会标记内存中的所有变量(表示要删除)。如果上下文中有变量,或者上下文中的变量引用了变量,则去掉标记。然后垃圾收集器清理所有标记的变量并回收内存。回到闭包,很明显垃圾回收器会将保存在包中的局部变量的标记去掉,不会被回收。闭包使用场景其实闭包你一定在很多地方见过。让我举几个常见的例子。防抖节流函数常见的防抖函数如下:if(timer){clearTimeout(timer)}timer=setTimeout(()=>{fn.apply(context,args)},wait)}}识别闭包的最快方法是找到两个元素:局部变量和对局部变量的函数。timer定时器不会随着函数debounce调用结束而被回收,而是一直在内存中。同理,我们再看一下throttle函数:functionthrottle(fn,wait){letprev=0//局部变量returnfunction(){//引用局部变量的函数letcontext=this,args=arguments,now=Date.now()if(now-prev>wait){fn.apply(context,args)prev=now}}}上面throttle函数中的局部变量prev和返回的匿名函数形成一个闭包。setTimeout的第一个参数setTimeout可以是一段可执行的JS代码,也可以是一个函数引用。如下:functionfn(name){//然后是一个局部变量returnfunction(){//匿名函数引用局部变量console.log(name)}}letgetName=fn('Joe')setTimeout(getName,1000)由于setTimeout的第一个参数是函数引用,所以不能带参数。那么就可以定义一个中间函数fn,将原本传递给getName的参数传递给fn,让fn返回一个函数作为setTimeout的第一个参数。在函数fn中,形参名和返回的匿名函数构成了一个闭包。Vue源码中的defineReactive函数。学习Vue2响应式原理的时候肯定见过defineReactive函数。在内部,Object.defineProperty用于实现响应性。//省略闭包函数之外的相关逻辑defineReactive(obj,key,value){returnObject.defineProperty(obj,key,{get(){returnvalue;},set(newVal){value=newVal;}})}局部变量obj、key、value会保存在内存中。闭包面试题闭包是前端面试的必考题,也是屡试不爽的。所以遇到这种问题,一是快速识别闭包的存在,二是清除内存中局部变量的变化。如何修改以下代码循环打印数字输出:12345?for(vari=1;i<=5;i++){setTimeout(functiontimer(){console.log(i)//实际输出55555},i*1000)}先搞清楚为什么输出55555.这里涉及到JS事件循环的知识。首先,定时器的回调属于宏任务。当代码执行到setTimeout时,它的回调会被放入宏任务队列中,循环5次,所以宏任务队列中有5个回调。异步队列中的任务需要等待主栈代码执行完毕后才会执行,也就是说这5个回调需要等待for循环执行完毕后才会执行;其次,for循环中的变量i是用var声明的,所以i是一个全局变量。当执行for循环时,全局i变成了5,此时会从异步队列中取出callback,顺序执行,所以连续打印5。我应该怎么解决这个问题?方法一:把var改成letfor(leti=1;i<=5;i++){setTimeout(functiontimer(){console.log(i)},i*1000)}其实let会形成一个块级范围,使i仅在for循环内成为局部变量。所以在每次循环中,局部变量i和函数timer组成一个闭包,一共5个闭包。这样每次循环的i都会被保存下来。方法二:使用立即执行函数for(vari=1;i<=5;i++){(function(j){//j为形参setTimeout(functiontimer(){//函数timer和j形成一个封闭的Packageconsole.log(j)},j*1000)})(i)//i是实参}原理还是用闭包。使用立即执行函数的目的是让它在声明后立即执行,其内部变量不会干扰外部。在每次循环中,通过传参将变量i传递给立即执行的函数的形参j,所以此时局部变量j和函数timer形成了一个闭包,变量j保存在内存中。查找运行结果例1查找以下代码的打印结果:vartest=(function(i){returnfunction(){console.log(i*=2)}})(2)test(5)答案是:4.首先,闭包由局部变量i和内部匿名函数组成,在立即执行函数内部。实参2赋值给形参i,所以内存中存入i=2。而在调用匿名函数,即test(5)时,参数5是没有作用的,即参数传不传都是一样的。展开一下,当连续执行两次test()时,打印出来的结果是什么?Print:48Example2要求以下代码打印结果:vara=0,b=0functionA(a){A=function(b){console.log(a+b++)}console.log(a++)}A(1)A(2)的答案是:14.这道题有点绕,因为A函数内部改变了A的方向。functionA(a){//a和function(b)形成一个闭包A=function(b){console.log(a+b++)}//a的值变为2并保存为闭包控制台的一部分.log(a++)}A(1)A(2)观察A函数,局部变量a和函数function(b){}形成一个闭包,但是并没有返回函数function(b){},如何让局部变量存储在内存中呢?当A(1)执行时,A函数中的语句A=function(b){}改变了指针A的指向,使其指向函数function(b){},所以函数function(b){}将是一个全局引用。这使得局部变量a存储在内存中。这时候先打印:1,然后自增,内存中a===2;执行A(2)时,A已经被改写,A=function(b){console.log(a+b++)},a===2,形参b为传入参数2,所以打印:4、复习:console.log(a++)先打印,后自增;console.log(++a)先递增,再打印;请参阅链接以了解Javascript闭包什么是JS中的闭包?