阿里云最近在搞活动,低至20折,有兴趣的可以去看看:https://promotion.aliyun.com/...为了保证更好的可读性,本文采用意译而非直译。正如标题所说,JavaScript闭包对我来说一直是个谜。我看过很多关于闭包的文章,在工作中使用过闭包,有时甚至在项目中使用过,但我确实在使用闭包。知识。最近看到的一些文章,终于有人用一种让我理解的方式解释了闭包,我将在本文中尝试使用这种方法来解释闭包。想阅读更多优质文章,请戳GitHub博客,一年百篇优质文章等你来!在理解闭包之前,有一个重要的概念需要先理解,就是js执行上下文。这篇文章是一篇很好的执行上下文入门教程。文章中提到:当代码在JavaScript中运行时,代码执行的环境非常重要,将归纳为以下几点:全局范围——代码第一次执行的默认环境。函数作用域——执行流进入函数体时。(...)——我们把执行上下文看作是当前代码执行的环境和范围。换句话说,当我们启动一个程序时,我们是从全局执行上下文开始的。一些变量在全局执行上下文中声明。我们称它们为全局变量。当程序调用函数时会发生什么?下面是几个步骤:JavaScript创建一个新的执行上下文,我们称之为本地执行上下文。这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。新的执行上下文被压入执行栈。将执行堆栈视为一种容器,它保存程序在其执行过程中的位置。函数什么时候结束?当它遇到返回语句或右括号}时。当函数结束时,会发生以下情况:这个本地执行上下文从执行堆栈中弹出。该函数将返回值返回给调用上下文。调用上下文是调用本地执行上下文,它可以是全局执行上下文或另一个本地执行上下文。此时要依赖调用执行上下文来处理返回值。返回值可以是对象、数组、函数、布尔值等,如果函数没有return语句,则返回undefined。这个本地执行上下文被销毁了,销毁很重要。在这个本地执行上下文中声明的所有变量都将被删除,并且不再有变量。这就是为什么它在本地执行上下文中被称为自己的变量。基本示例在讨论闭包之前,让我们看一下下面的代码:1:leta=32:functionaddTwo(x){3:letret=x+24:returnret5:}6:letb=addTwo(a)7:console.log(b)为了理解JavaScript引擎是如何工作的,让我们详细分析一下:在第1行,我们在全局执行上下文中声明了一个新变量a并赋值3。然后就变得棘手了,第2行通5其实一起走。这里发生了什么?我们在全局执行上下文中声明了一个名为addTwo的新变量,我们为它分配了什么?函数定义。两个括号{}之间的任何内容都分配给addTwo,函数内部的代码不会被评估,不会被执行,只是存储在一个变量中以备将来使用。现在我们在第6行。它看起来很简单,但这里有很多东西需要拆开。首先,我们在全局执行上下文中声明一个新变量并将其标记为b。一旦变量被声明,它的值是未定义的。接下来,还是在第6行,我们看到一个赋值运算符。我们即将为变量b分配一个新值,接下来我们看到一个函数被调用。当您看到变量后跟圆括号(...)时,这是调用函数的信号。接下来,每个函数都会返回一些东西(值、对象或未定义),并且从函数返回的任何内容都将分配给变量b。但首先我们需要调用addTwo函数。JavaScript将在其全局执行上下文内存中查找名为addTwo的变量。哦,它找到了一个,它是在第2步(或第2-5行)中定义的。变量add2包含函数定义。请注意,变量a作为参数传递给函数。JavaScript在全局执行上下文内存中搜索变量a,找到它,发现它的值为3,并将数字3作为参数传递给函数,准备执行函数。现在执行上下文将切换,创建一个新的本地执行上下文,我们将其命名为“addTwo执行上下文”,并将执行上下文压入调用堆栈。在addTwo执行上下文中,我们做的第一件事是什么?您可能会说,“在addTwo执行上下文中声明了一个新变量ret”,这是不正确的。正确答案是我们需要先看函数的参数。在addTwo执行上下文中声明了一个新变量x`,并且由于值3作为参数传递,因此变量x被分配了值3。下一步是:在addTwo执行上下文中声明一个新变量ret。它的值设置为未定义(第三行)。还是在第3行,需要进行加法运算。首先我们需要x的值,JavaScript会寻找一个变量x,它会先在addTwo执行上下文中寻找,找到一个值3。第二个操作数是数字2。两次相加的结果是5和被分配给变量ret。第4行,我们返回变量ret的内容,在addTwo执行上下文中搜索,找到值5,返回,函数结束。第4-5行,函数结束。addTwo执行上下文被销毁,变量x和ret被释放,它们不再存在。addTwo执行上下文从调用堆栈弹出,返回值返回到调用上下文,在本例中是全局执行上下文,因为函数addTwo是从全局执行上下文调用的。现在我们继续第4步的内容,将返回值5赋值给变量b,程序还在第6行,第7行将b的值5打印到控制台。这是对一个非常简单的程序的非常冗长的解释,我们甚至还没有接触到闭包。但它肯定会涉及到,但首先我们得绕一两道弯路。词法作用域(Lexicalscope)我们需要了解一些词法作用域的知识。请看下面的例子:1:letval1=22:functionmultiplyThis(n){3:letret=n*val14:returnret5:}6:letmultiplied=multiplyThis(6)7:console.log('exampleofscope:',multiplied)这里要说明一下,我们有函数执行上下文中的变量和全局执行上下文中的变量。JavaScript的一个复杂问题是它如何查找变量,如果它在函数执行上下文中找不到变量,它会在调用上下文中查找它,如果它在调用上下文中找不到它,它就会上升一级直到它在全局执行上下文中查找。(如果最后没找到,就是undefined)。下面列出以下步骤进行解释(熟悉的可以跳过):在全局执行上下文中声明一个新的变量val1,并为其赋值2。2-5行,声明一个新的变量multiplyThis,并为其赋值一个函数定义.第6行,在全局执行上下文中声明了一个新变量multiplied。从全局执行上下文内存中查找变量multiplyThis并将其作为函数执行,将数字6作为参数传递。新的函数调用(创建新的执行上下文),创建一个新的multiplyThis函数执行上下文。在multiplyThis执行上下文中,声明一个变量n并为其赋值6。第3行。在multiplyThis执行上下文中,声明一个变量ret。继续第3行。对两个操作数n和val1执行乘法运算。在multiplyThis执行上下文中查找变量n。我们在第6步中声明了它,它的内容是数字6。在multiplyThis执行上下文中查找变量val1。multiplyThis执行上下文没有标记为val1的变量。我们查看调用上下文,即全局执行上下文,并在全局执行上下文中查找val1。哦对了,就是这样,在步骤1中定义,值为2。继续第3行,将两个操作数相乘,赋值给ret变量,6*2=12,ret现在是12。返回ret变量,销毁multiplyThis执行上下文及其变量ret和n。变量val1没有被销毁,因为它是全局执行上下文的一部分。回到第6行。在调用上下文中,数字12被分配给multiplied变量。最后在第7行,我们将相乘变量的值打印到控制台。在这个例子中,我们需要记住一个函数可以访问在它的调用上下文中定义的变量,这是词法范围。返回函数的函数在第一个示例中,函数addTwo返回一个数字。记住,函数可以返回任何东西。让我们看一个返回函数的函数示例,因为这对于理解闭包非常重要。参见Chestnut:1:letval=72:functioncreateAdder(){3:functionaddNumbers(a,b){4:letret=a+b5:returnret6:}7:returnaddNumbers8:}9:letadder=createAdder()10:letsum=adder(val,8)11:console.log('exampleoffunctionreturningafunction:',sum)让我们回到分步分析:第1行。我们在全局执行上下文中声明一个变量val并赋值7。第2-8行。我们在全局执行上下文中声明了一个名为createAdder的变量,并为其分配了一个函数定义。第3-7行描述了上面的函数定义,和以前一样,此时我们不直接讨论函数。我们只是将函数定义存储到该变量(createAdder)中。第9行。我们在全局执行上下文中声明了一个名为adder的新变量,目前,该变量的值是未定义的。第9行,我们看到括号(),我们需要执行或调用一个函数,查找全局执行上下文的内存并寻找一个名为createAdder的变量,该变量是在步骤2中创建的。那么,让我们调用它。当函数被调用时,执行转到第2行。创建一个新的createAdder执行上下文。我们可以在createAdder的执行上下文中创建自己的变量。js引擎将createAdder的上下文添加到调用栈中。这个函数没有参数,所以让我们直接跳到函数体。第3-6行。我们有一个新的函数声明,我们在createAdder执行上下文中创建了一个变量addNumbers。这很重要,addnumber仅存在于createAdder执行上下文中。我们将函数定义存储在一个名为addNumbers的自有变量中。在第7行,我们返回变量addNumbers的内容。js引擎找一个叫addNumbers的变量,找到了,这是一个函数定义。嗯,函数可以返回任何东西,包括函数定义。我们返回addNumbers的定义。第4行和第5行括号之间的内容构成了函数定义。返回时,createAdder执行上下文将被销毁。addNumbers变量不再存在。但是addNumbers函数定义仍然存在,因为它返回并赋值给adder变量。第10行,我们在全局执行上下文中定义了一个新的变量sum,首先赋值为undefined;接下来我们需要执行一个函数。哪个功能?是在名为adder的变量中定义的函数。我们在全局执行上下文中查找它,果然我们找到了它,这个函数有两个参数。我们来看这两个参数,第一个是我们在步骤1中定义的变量val,代表数字7,第二个是数字8。现在我们要执行这个函数。函数定义总结在第3-5行,因为这个函数是匿名的,为了便于理解,暂且称它为adder。此时创建了一个adder函数执行上下文,并在adder执行上下文中新建了两个变量a和b。它们分别被赋值为7和8,因为那些是我们在上一步中传递给函数的参数。第4行,在adder执行上下文中声明了一个名为ret的新变量,第4行,将变量a的内容和变量b的内容相加得到15,赋值给ret变量。ret变量从函数返回。匿名函数执行上下文被销毁,从调用栈中移除,变量a、b、ret不复存在。返回值被分配给我们在步骤9中定义的sum变量。我们将sum的值打印到控制台。控制台按预期打印15。我们在这里确实经历了很多困难,在这里我想说几点。首先,函数定义可以存储在变量中,函数定义在程序调用之前是不可见的。其次,每次调用函数时,都会(临时)创建一个本地执行上下文。当函数完成时,执行上下文消失。该函数在遇到返回或右括号}时执行。最后,一个闭包看看下面的代码,试着弄清楚会发生什么。1:functioncreateCounter(){2:letcounter=03:constmyFunction=function(){4:counter=counter+15:returncounter6:}7:returnmyFunction8:}9:constincrement=createCounter()10:constc1=increment()11:constc2=increment()12:constc3=increment()13:console.log('exampleincrement',c1,c2,c3)现在,我们已经从之前的两个例子现在我们已经掌握了它的窍门,让我们按预期快速执行它:第1-8行。我们在全局执行上下文中创建了一个新变量createCounter并为其分配了一个函数定义。第9行。我们在全局执行上下文中声明了一个名为increment的新变量。第9行,我们需要调用createCounter函数并将其返回值赋给increment变量。第1-8行。调用一个函数,创建一个新的本地执行上下文。Line2.在本地执行上下文中,声明一个名为counter的新变量,并赋值为0;第3-6行。声明一个名为myFunction的新变量。该变量在本地执行上下文中声明。变量的内容在第4行和第5行定义。第7行。返回myFunction变量的内容,删除本地执行上下文。变量myFunction和counter不再存在。此时控制返回到调用上下文。第9行,在调用上下文(全局执行上下文)中,将createCounter返回的值赋值给increment,变量increment现在包含一个函数定义,其内容由createCounter返回。它不再标记为myFunction`,但其定义是相同的。在全局上下文中,它被标记为labeledincrement。第10行。声明一个新变量c1。继续第10行。找到increment变量,它是一个函数并调用它。它包含先前返回的函数定义,如第4-5行所定义。创建一个新的执行上下文。没有参数,开始执行函数。第4行。计数器=计数器+1。在本地执行上下文中查找计数器变量。我们只是创建了那个上下文,从未声明过任何局部变量。让我们看看全局执行上下文。这里也没有计数器变量。Javascript会将其计算为counter=undefined+1,声明一个新的局部变量标记为counter,并将其赋值为1,因为undefined被视为值0。第5行。我们的变量counter的值为1,我们销毁本地执行上下文和计数器变量。回到第10行,将返回值1赋值给c1。第11行,重复10-14步,c2也被赋值为1。12行,重复10-14步,c3也被赋值1。13行,我们打印变量c1c2和c3的内容。自己尝试一下,看看会发生什么。您会注意到它没有按照我上面的解释预期的那样记录1,1,1。相反,它记录1、2、3。为什么是这样?不知何故,增量函数记住了那个计数器的值。这里发生了什么?counter是全局执行上下文的一部分吗?尝试console.log(counter)并得到undefined,这显然不是这种情况。也许,当您调用increment时,它会以某种方式返回它创建的函数(createCounter)?这怎么可能?变量increment包含函数定义,而不是函数的源代码,显然也不是这样。所以必须有另一种机制。关闭,我们终于找到了缺失的部分。这是它的工作原理,每当您声明一个新函数并将其分配给一个变量时,存储函数定义和闭包。闭包包含创建函数时范围内的所有变量,它类似于背包。函数定义带有一个小背包,用于存储创建函数定义时范围内的所有变量。所以我们上面的解释都是错误的,我们再试一次,但是这次是正确的。1:functioncreateCounter(){2:letcounter=03:constmyFunction=function(){4:counter=counter+15:returncounter6:}7:returnmyFunction8:}9:constincrement=createCounter()10:constc1=increment()11:constc2=increment()12:constc3=increment()13:console.log('exampleincrement',c1,c2,c3)同上,第1-8行.我们在获取指定函数定义的全局执行上下文中创建一个新变量createCounter。同上,第9行。我们在全局执行上下文中声明了一个名为increment的新变量。同上,第9行,我们需要调用createCounter函数,并将其返回值赋值给increment变量。同上,第1-8行。调用一个函数,创建一个新的本地执行上下文。同上,第2行。在本地执行上下文中,声明一个名为counter的新变量并为其赋值0。第3-6行。声明一个名为myFunction的新变量。该变量在本地执行上下文中声明。变量的内容是另一个函数定义。正如第4行和第5行所定义的,我们现在还创建了一个闭包作为函数定义的一部分。闭包包含范围内的变量,在本例中为变量计数器(值为0)。第7行。返回myFunction变量的内容,删除本地执行上下文。myFunction和counter不再存在。控制被传递到调用上下文,我们返回函数定义及其闭包,其中包含创建时范围内的变量。Line9.在调用上下文(全局执行上下文)中,createCounter返回的值被指定为increment,变量increment现在包含了createCounter返回的函数定义的函数定义(和闭包),它不再被标记为myFunction,但它的定义是相同的,在全局上下文中,称为增量。第10行。声明一个新变量c1。继续第10行。寻找变量增量,这是一个函数,调用它。它包含先前返回的函数定义,如第4-5行所定义。(它还有一个带变量的闭包)。创建一个新的执行上下文,不带任何参数,然后开始执行该函数。第4行counter=counter+1,寻找变量counter,在寻找局部或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为counter的变量,值为0。在行的表达式之后4,它的值设置为1。它再次存储在闭包中,现在包含值为1的变量counter。第5行。我们返回counter的值,破坏本地执行上下文。回到第10行,将返回值1赋值给变量c1。第11行。我们重复步骤10-14。这一次,在闭包中此时变量counter的值为1。它在第12行设置,它的值递增并在增量函数的闭包中存储为2,并且c2被赋值2。第12行。重复步骤10-14,c3被赋值3。第13行。我们打印变量c1c2和c3的值。你可能会问,任何函数都有闭包吗,即使是在全局范围内创建的函数?答案是肯定的。在全局范围内创建的函数会创建闭包,但是由于这些函数是在全局范围内创建的,它们可以访问全局范围内的所有变量,因此闭包的概念并不重要。当函数返回函数时,闭包的概念变得更加重要。返回的函数可以访问不属于全局范围的变量,但它们只存在于它的闭包中。闭包并不那么简单有时闭包出现时你甚至没有注意到它,你可能已经看到我们称之为部分应用的示例,如下面的代码所示:letc=4constaddX=x=>n=>n+xconstaddThree=addX(3)letd=addThree(c)console.log('examplepartialapplication',d)如果箭头函数让你感到困惑,这里有同样的效果:letc=4functionaddX(x){returnfunction(n){returnn+x}}constaddThree=addX(3)letd=addThree(c)console.log('examplepartialapplication',d)我们声明一个可以使用addX的加法函数,它接受一个参数x和返回另一个函数。返回的函数也接受一个参数并将其添加到变量x。变量x是闭包的一部分,当在局部上下文中声明变量addThree时,它??会被分配一个函数定义和一个包含变量x的闭包。所以当addThree被调用和执行时,它可以访问闭包中的变量x以及作为参数传递的变量n并返回两者之和,7。我将永远记住闭包的方式是通过背包类比.当一个函数被创建并从另一个函数传递或返回时,它背负着一个背包。背包中是声明函数时范围内的所有变量。代码部署后可能存在的bug,无法实时获知。事后为了解决这些bug,花费了大量的时间在日志调试上。顺便推荐一个好用的bug监控工具Fundebug。你的点赞是我继续分享好东西的动力,欢迎点赞!干货交流系列文章总结如下。我觉得点个Star就好了。欢迎加群,互相学习。https://github.com/qq44924588...我是小智,公众号《大招天下》的作者,前端技术爱好者。我会经常分享自己学习看到的干货,在进步的路上互相鼓励!关注公众号,后台回复福利,就能看到福利,你懂的。
