当前位置: 首页 > 科技观察

浅谈对JavaScript闭包的理解

时间:2023-03-18 22:11:53 科技观察

在谈闭包之前,我们首先要了解几个概念:什么是函数表达式?它与函数声明有何不同?JavaScript寻找标识符的机制JavaScript的作用域是词法作用域JavaScript的垃圾回收机制首先来说说函数表达式。什么是函数表达式?如果function是声明中的第一个单词,则它是一个函数声明,否则它是一个函数表达式。例如:varfoo=function(){};//匿名函数表达式(functionfoo(){})()//函数表达式,因为function不是声明的第一个字,所以有个"("functionfoo(){}//函数声明函数表达式也分为匿名函数表达式和命名函数表达式:varfoo=function(){}//匿名函数表达式varfoo=functionbar(){}//命名函数命名表达式需要注意的一点函数表达式:上例中的bar标识符只存在于当前函数作用域,不存在于全局作用域。函数声明和函数表达式的重要区别是:函数声明有了函数声明提升,函数表达式就不会函数表达式可以在表达式后加括号立即执行,但是函数声明不能??(function(){})()//匿名函数表达式,这种Mode函数,usu盟友称为IIFE(ImmediatelyInvokedFunctionExpresstion)代表立即执行函数表达式。函数和变量声明的改进这里就不多说了。想了解的同学可以参考相关资料JavaScript何时执行函数标识符的查找机制对作用域链和变量对象不了解的同学可以先查阅相关资料再看。作用域链本质上是一个指向变量对象的指针列表,只引用但实际上并不包含变量对象。变量、函数等都存在于各自作用域的变量对象中,通过访问变量对象来访问。只有在函数被调用的时候,才会创建执行环境和作用域链,而每个环境只能一步步向上查找作用域链,查找变量名、函数名等标识符。JavaScript的作用域JavaScript的作用域是词法作用域而不是动态作用域。词法作用域最重要的特点是它的定义过程发生在代码的编写阶段。动态作用域的作用域链是基于调用栈的词法作用域。作用域链是基于作用域嵌套的functionfoo(){console.log(num)}functionbar(){varnum=2;foo();//1}varnum=1;bar();bar函数在代码中的时候执行时,foo函数会被执行,因为JavaScript是词法作用域,所以函数执行时,会在定义时沿着作用域链搜索变量,而不是执行时,foo函数定义在全局,所以找到全局num,它输出1而不是2。让我们谈谈闭包。什么是闭包其实有很多种说法,就看你自己的理解了。主要有两种类型:NicolasC.Zakas:闭包是指一个函数有权访问另一个函数的作用域。变量的函数KYLESIMPSON:当一个函数可以记住并访问它所在的词法作用域时,就会创建一个闭包。该函数持有对词法作用域的引用。此引用称为闭包。我个人更喜欢后者或者闭包的定义,即闭包是一个引用。让我们看一些代码:functionfoo(){vara=5;returnfunction(){console.log(a);}}varbar=foo();bar();//5在这段代码中,foo将返回一个匿名函数expression,这个函数可以访问foo()的作用域,引用可以引用它,然后把这个匿名函数赋值给变量bar,这样bar就可以引用这个匿名函数,可以调用了。在此示例中,匿名函数在其自己定义的词法范围之外成功执行。这就是闭包强大的地方,比如通过闭包实现模块模式:;obj.doSomething()//module我们通过调用aModule函数创建了一个模块实例。函数返回的对象本质上可以看成是这个模块的公告API。是不是有点像其他面向对象的语言?班级?然后通过闭包实现单例模式:varapplication=function(){varcomponents=[];/*一些初始化操作*/return{//publicAPIgetComponentCount:function(){returncomponents.length;},registerComponent:function(component){components.push(component);}};}();这个例子通过IIFE创建了一个单例对象,函数中返回的对象字面量就是这个单例模式的公共接口。通过闭包实现模块模式可以做很多强大的事情。模块模式可以成功实现。最重要的是返回的API可以继续引用定义它的作用域,从而进行一些操作,也就是作用域不会因为函数执行完就销毁了,也就是不会被记忆回收。之所以不被回收,是因为闭包的存在和JavaScript的垃圾回收机制。JavaScript的垃圾回收机制JavaScript中最常用的垃圾回收方法是标记清除。垃圾收集器会标记所有存储在内存中的变量,然后移除环境中的变量和环境中变量引用的变量。标记,说明这些变量还有用,暂时不能删除,然后后面标记的变量就是要删除的变量,等待垃圾回收器为它们完成清理工作。对于函数来说,函数执行完后,里面的变量会自动释放,但是如果函数内部有闭包,是不会被删除的,因为这个函数还是被函数内部引用的,所以不会与mark,不会被清除,而是会一直存在于内存中,无法释放!除非使用闭包的内部函数被销毁,外部函数才能被释放。因此,闭包虽然强大,但我们不能滥用它。并且非必要尽量不要创建闭包,否则会出现大量变量对象得不到释放而过度占用内存。关于循环和闭包当循环和闭包结合在一起时,经常会出现让初学者觉得不可思议的问题。我们来看《JavaScript高级程序设计》中NicolasC.Zakas的一段代码:functioncreateFunction(){varresult=[];for(vari=0;i<10;i++){result[i]=function(){returni;};}returnresult;}该函数执行后,会创建一个由十个函数组成的数组,并生成十个独立的函数作用域。表面上看函数调用次数会输出几个,但是结果不是这样的varresult=createFunction();result[0]();//10result[9]();//10原因奇怪的现象是createFunction的变量对象因为闭包没有被释放,注意闭包保存的是整个变量对象,而不仅仅是引用的变量。createFunction执行后,创建了十个函数,变量i并没有被释放,仍然保存在内存中,所以它的值在停止循环后仍然是10。当我们从外部调用一个函数时,该函数开始沿着它的作用域链搜索所需的变量。前面说过,JavaScript的作用域链是基于定义时的作用域嵌套的,所以当我们调用result[0]这样的函数时,它会先通过RSH在自己的作用域中查找i,显然i并不存在于这个作用域,所以它沿着作用域链在上层作用域中查找i,然后找到i,但是此时已经执行了createFunction函数,循环执行完毕。i的值为10,所以得到的i的值为10。同理,执行其他函数时,查找到的i也会是10,所以每次函数执行的结果都是输出10。关键在于,循环中的十个函数虽然是在各自的迭代中定义的,但是它们都在一个共享的上层作用域中,所以它们都得到一个i,所以解决此类问题的关键在于,当函数搜索i,并没有找到createFunction变量对象的level,因为一旦搜索到createFunction,就会得到10。所以我们可以使用一些方法来截断应该搜索createFunction变量对象的搜索在中间。首先我们可以这样做:functioncreateFunction(){varresult=[];for(vari=0;i<10;i++){(function(){result[i]=function(){returni;};})();}returnresult;}我们通过定义立即执行函数表达式在result[i]函数之上创建一个块级作用域。如果我们把这个块级作用域称为a,那么它在寻找i的时候,看起来是这样的链式result[i]->a->createFunction,之所以会找到createFunction,是因为a中没有变量i,所以我们需要做一些事情让它在搜索函数时停止createFunctions(){varresult=newArray();for(vari=0;i<10;i++){(function(i){result[i]=function(){returni;};})(i);}returnresult;}现在在这个块级范围内定义了一个变量i。这个i不会和上级i交互,因为它们存在于自己的范围内。同时,我们将本次迭代中i的值赋值给a的block-levelaction字段中的i,即a中的i保存本次迭代的i,当result[i]为在外部执行,调用链resulti->a可以在a中找到需要的变量,不需要向上查找,也不会找到值为10的i,所以无论调用哪个result[i]函数,都会输出whichi。在ES6中我们也可以使用let来解决这样的问题};}returnresult;}//输出console.log(createFunction()[2]());//2let会创建一个块级作用域,并在这个作用域中声明一个变量。所以我们相当于在result[i]上设置了一层块级作用域functioncreateFunction(){varresult=[];for(vari=0;i<10;i++){//块的开头letj=i;result[i]=function(){returnj;};//block结束}returnresult;}这个方法解决的就是这种问题,和上一个没有太大区别。简而言之,就是防止函数搜索最顶层的。我。事实上,如果在for循环的头部进行let语句,会出现一个有趣的行为:functioncreateFunction(){varresult=[];for(leti=0;i<10;i++){//每次迭代,会声明一次i,一共声明10次result[i]=function(){returni;};}returnresult;}console.log(createFunction()[2]());//2使用for头中的let语句,为每次迭代进行声明,随后的每次迭代都使用上一次迭代结束时的值初始化变量。其实函数作为值类型来回传递的时候,基本都会用到闭包,比如定时器,跨窗口通讯,事件监听,ajax等等,基本上只要用到回调函数,其实都是用到闭包。关闭是一把双刃剑。它是JavaScript中较难理解和掌握的部分。它非常强大,但也有很大的缺陷。如何使用它完全取决于您。以上均为个人观点,如有不妥请指正。