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

深入理解什么是JavaScript闭包闭包

时间:2023-04-02 13:51:14 HTML

前言在阅读本文之前,可以先阅读前面几篇文章,深入理解JavaScript执行上下文和JavaScript作用域。了解执行上下文和范围对于理解闭包非常有帮助。需要回忆的一些知识点:作用域和词法作用域,作用域是一组查找变量的规则(在哪里查找,如何查找)。词法作用域是在您编写代码时建立的。JavaScript是一种基于词法作用域的语言,通过变量定义的位置可以知道变量的作用域。作用域链:当函数第一次被调用时,会创建一个执行环境和对应的作用域链,并将作用域链赋值给一个特殊的内部属性[[Scope]]。然后使用this、arguments和其他命名参数的值初始化函数的活动对象。但是在作用域中,外层函数的存活对象永远是第二位,外层函数的存活对象是第三位,……直到作用域链末端的全局执行环境。一个真实的面试场景A:什么是闭包B:在函数foo内部声明了一个变量a,在函数外部无法访问。闭包允许在函数外访问函数内的变量A:呃,不完全是,那就告诉我闭包是干什么用的。B:……对不起,我一下子想不起来了。A:今天的采访到此结束,有后续会通知大家。闭包几乎是面试必问的一个知识点。记得几年前刚出来找实习的时候就问过这个问题,现在出去面试的时候还一直问这个问题。一定要学好它,不仅因为面试,还因为它在代码中也很常见。什么是闭包?当一个函数可以记住并访问它所在的词法范围时,就会发生闭包,即使该函数是在当前词法范围之外执行的。函数foo(){变量a=1;//a是由foofunctionbar()创建的局部变量{//bar是一个内部函数和一个闭包console.log(a);//使用函数中声明的父变量}returnbar();}foo();//1foo()声明了一个内部变量a,函数外不能访问,bar()是foo()函数内部的一个函数,此时foo内部的所有局部变量对bar都是可见的,反之亦然,bar内的局部变量对foo是不可见的。这就是javaScript特有的“作用域链”。函数foo(){变量a=1;//a是由foofunctionbar()创建的局部变量{//bar是一个内部函数和一个闭包console.log(a);//使用parent函数中声明的变量}returnbar;}constmyFoo=foo();myFoo();这段代码和上面的代码完全一样,不同的是内部函数bar在执行前从外部函数返回。foo()执行后,将其返回值(即内部bar函数)赋值给变量myFoo,并调用myFoo(),实际上只是调用了内部函数bar(),使用了不同的标识符引用。foo()函数执行后,正常情况下,foo()的整个内部作用域被销毁,占用的内存被回收。但是现在foo的内部作用域bar()还在使用,所以不会被回收。bar()仍然持有对新作用域的引用,这个引用称为闭包。这个函数是从定义的词法范围之外调用的。闭包允许函数继续访问定义它们的词法范围。一句话:闭包是一个函数,它可以访问另一个函数范围内的变量。创建闭包最常见的方法是在另一个函数中创建一个函数。一些常见的闭包functionfoo(a){setTimeout(functiontimer(){console.log(a)},1000)}foo(2);foo执行1000ms后,其内部作用域不会消失,timer函数仍然保留对foo作用域的引用。定时器函数是一个闭包。在定时器、事件监听器、Ajax请求、跨窗口通信、WebWorkers或其他异步或同步任务中,只要使用到回调函数,其实就是一个闭包。循环和关闭for(vari=0;i<5;i++){setTimeout(()=>{console.log(i);},i*1000);}上面的代码期望每1秒,0,分别输出1、2、3、4,实际上是依次输出5、5、5、5、5。先说明5从何而来。这个循环的终止条件是i不再<5。当条件第一次为真时i的值是5。因此,输出显示i在循环结束时的最终值。直到循环结束才会执行延迟函数的回调。事实上,即使在定时器运行时每次迭代都执行setTimeout(..,0),所有的回调函数在循环结束后仍然会被执行。所以每次输出一个5。我们的期望是每次迭代都会在运行时“捕获”自己的i副本。但实际上,根据作用域原则,虽然循环中的5个函数是在各自的迭代中定义的,但它们都封装在一个共享的全局作用域中,所以实际上只有一个i。也就是说,所有函数共享对i的引用。for(vari=0;i<5;i++){(function(j){setTimeout(()=>{console.log(j);},j*1000);})(i)}代码是改成了这样,我们就可以按照我们期望的方式工作了。这样修改后,在每次迭代中使用IIFE(立即执行函数)会为每次迭代生成一个新的作用域,这样延迟函数的回调就可以在每次迭代中封装新的作用域,每次迭代都会包含一个具有正确值的变量可以访问。for(leti=0;i<5;i++){setTimeout(()=>{console.log(i);},i*1000);}用ES6块级的let代替var也可以实现我们的的目标。为什么闭包在JavaScript中的应用总是有“return”这个关键字,javaScript秘密花园中的一段解释道:闭包是JavaScript的一个非常重要的特性,它意味着当前作用域始终可以访问外层作用域的变量。因为函数是JavaScript中唯一拥有自己作用域的结构,所以闭包的创建依赖于函数。注意点很容易导致内存泄漏。闭包带有封闭它们的函数的作用域,因此它们比其他函数占用更多的内存。过度使用闭包会导致过多的内存使用,因此请谨慎使用闭包。这种情况在闭包中使用this对象。this对象根据函数的执行环境在运行时绑定。在全局函数中,this指向window。当函数被作用于对象的方法调用时,this指向这个对象。但是匿名函数的执行环境是全局的,所以它的this对象通常指向window。之前理解这个,call,apply,bind的文章也专门讲过这个。varname='Thewindow';varobject={name:'myObject',getName:function(){returnfunction(){returnthis.name;}}}console.log(object.getName()());//上面代码在window的非严格模式下创建了一个全局变量name,并创建了一个包含name属性的对象,这个对象还包含一个方法getName(),返回一个匿名函数,匿名函数返回this.name。由于getName返回一个函数,调用object.getName()()会立即调用它返回的函数。结果是字符串“Thewindow”,即全局名称变量的值。为什么匿名函数得不到包含作用域的this对象?每个函数在调用时都会自动获得两个特殊变量:this、arguments。内部函数查找这两个变量时,只会查找到它的活动对象,所以永远不可能直接访问外部函数的这两个变量。但是将this对象存储在闭包可访问的变量中的外部范围内允许闭包访问该对象。varname='Thewindow';varobject={name:'myObject',getName:function(){varthat=this;//将这个对象赋给那个变量returnfunction(){returnthat.name;}}}console.log(object.getName()());//上面my??Object的代码中,this对象赋值给that变量,that变量包含在函数中。即使在函数返回后,它仍然被引用Thisisanobject,所以调用object.getName()()返回“myObject”参数,这有同样的问题。如果要访问作用域中的参数对象,必须将对该对象的引用保存到另一个闭包中,以便能够在变量中访问它。在一些特殊情况下,this的值可能会意外更改。例如,以下代码是修改其先前示例的结果。varname='Thewindow';varobject={name:'myObject',getName:function(){returnthis.name}}console.log(object.getName());//我的Objectconsole.log((object.getName)());//我的Objectconsole.log((object.getName=object.getName)());//在window的非严格模式下第一次调用是正常调用,打印"myObject"第二次是在调用这个方法之前加上括号,但是和object.getName是一样的,所以是打印为“我的对象”。三是先执行一条赋值语句,然后调用赋值的结果。因为这个赋值表达式是函数本身,所以这时候调用的时候,this指向的是窗口,打印的是“Thewindow”。关于什么是闭包,这是关于它的。下一篇文章会讲到闭包的应用场景。.总结一下,闭包是一个函数,它可以访问另一个函数范围内的变量。闭包通常用于创建内部变量,使这些变量不能被外界随意修改,同时又可以通过指定的接口进行操作。参考Crackingthefront-endinterview(80%ofapplicantsfailtheseries):SpeakingofclosuresMDN-ClosuresLearningJavascriptClosures(Closure)闭包详解UnderstandingClosures一直不懂JavaScript闭包,直到有人问我才解释说最近推出100天前端进阶计划,主要是深入挖掘每个知识点背后的原理,欢迎关注微信公众号“牧马之星”,一起学习,签到100天。同时也会分享一些自己的学习心得和想法,欢迎大家交流。