什么是JavaScript闭包?
JavaScript已经用了一年多,闭包总是让人迷惑。我一个接一个接触了一些闭包的知识,因为不懂闭包,犯了好几个错误。有些资料我看了一年多,还是不太懂。最近偶然看了一下jQuery基础教程的附录,发现附录A中JavaScript中闭包的介绍简单易懂,于是借花献佛进行总结。
1.定义。
闭包是指一个函数可以访问另一个函数范围内的变量。创建闭包的一种常见方法是在另一个函数中创建一个函数。
上面的直接例子。
函数a(){ var I=0;函数b(){ alert(I);}返回b;} var c=a();c();该代码有两个特征:
1)函数b嵌套在函数a内部;2)函数a返回函数b。
这样,执行var c=a()后,变量c实际指向函数b,执行c()后,会弹出一个窗口,显示I(第一次为1)的值。这段代码实际上创建了一个闭包。为什么呢?因为函数A外部的变量C引用函数A内部的函数B,也就是当函数A内部的函数B被函数A外部的变量引用时,就创建了闭包。
我猜你还是不懂闭包,因为你不知道它们是干什么的。让我们继续探索。
2.闭包的作用是什么?
简单来说,闭包的作用就是在A完成执行并返回后,闭包使得Javascript的垃圾收集机制GC不回收A占用的资源,因为A的内部函数B的执行依赖于A中的变量,这是对闭包的作用非常直白的描述,不专业也不精确,但很可能就是这个意思。理解闭包需要一个渐进的过程。在上面的例子中,由于闭包的存在,函数a返回后我总是存在于a中,所以每次执行c()时,我都是alert加1后的值。
让我们想象另一种情况。如果A不返回函数B,情况就完全不同了。因为A执行后,B不返回A的外部世界,只被A引用,此时A也只会被B引用。所以函数A和B相互引用,不受外界干扰(被外界引用),函数A和B会被GC回收。(后面会详细介绍Javascript的垃圾收集机制。)
3.闭包中的微观世界。
如果我们想更多地了解闭包以及函数A和嵌套函数B之间的关系,我们需要引入其他几个概念:函数执行上下文、调用对象、作用域和作用域链。以函数A从定义到执行的过程为例来说明这些概念。
1)定义函数a时,js解释器会将函数a的作用域链设置为定义a时a所在的“环境”,如果a是全局函数,则作用域链中只有window对象。2)当函数a被执行时,a将进入相应的执行上下文。3)在创建执行环境的过程中,首先会给A增加一个范围属性,也就是A的范围,它的值就是步骤1中的范围链。也就是a.scope=a的范围链.4)然后执行环境将创建一个调用对象。活动也是一个带有属性的对象,但是它没有原型,不能通过JavaScript代码直接访问。创建活动对象后,将活动对象添加到A的范围链的顶部。此时,的范围链包含两个对象:的活动对象和窗口对象。5)下一步是在活动对象上添加一个arguments属性,保存调用函数A时传递的参数6)最后,将函数A的所有形式参数和函数B的内部引用添加到A的活动对象中,这一步完成了函数B的定义,所以在第三步中,将函数B的作用域链设置为B定义的环境,即A的作用域。
至此,整个函数a从定义到执行的步骤完成。此时a返回函数b对c的引用,函数b的作用域链包含函数a的活动对象的引用,这意味着b可以访问a中定义的所有变量和函数.函数b被c引用,依赖于函数a,所以函数a返回后不会被GC回收。
当执行函数b时,它将与上述步骤相同。因此,在执行时,B的范围链包含三个对象:B的活动对象、A的活动对象和窗口对象。在函数B中访问一个变量时,搜索顺序是先搜索自己的活动对象,如果存在就返回;如果不存在,它将继续搜索函数A的活动对象,并依次搜索,直到找到为止。如果在整个范围链中找不到它,它将返回undefined。如果函数b中有原型对象,在找到自己的活动对象后找到自己的原型对象,然后继续寻找。这是Javascript中的变量搜索机制。
4.闭包的应用场景。
1)、保护函数中变量的安全。以初始例子为例,函数A中的I只能被函数B访问,而不能被其他手段访问,从而保护了I的安全性2)在内存中维护一个变量。和前面的例子一样,因为闭包的原因,函数A中的I总是存在于内存中,所以每次执行C()时,我都会被加1。以上两点是闭包最基本的应用场景,很多经典案例都源于此。
5.Javascript的垃圾收集机制。
在Javascript中,如果一个对象不再被引用,它将被GC回收。如果两个对象相互引用,并且不再被第三个引用,那么这两个相互引用的对象也将被回收。因为函数A被B引用,而B在A之外被C引用,这就是函数A执行后不会被回收的原因。
javascript中没有块级作用域。一般为了声明一些只能被一个函数使用的局部变量,我们会使用闭包,这样可以大大减少全局范围内的变量,净化全局范围。
使用闭包有以上优点,当然这样的优点需要付出代价,而代价就是内存占用。
如何理解上面这句话?
当一个函数被执行时,与该函数相关的函数执行环境或上下文被创建。在这个执行上下文中有一个属性范围链(范围链指针),它指向一个范围链结构,并且范围链中的指针都指向对应于每个范围的活动对象。通常,函数在调用开始执行时创建函数执行上下文和相应的作用域链,并在函数执行结束后释放函数执行上下文和相应的作用域链所占用的空间。
//声明函数functiontest(){ var str=' hello world ';console . log(str);}//调用函数test();调用函数时,会在内存中生成下图所示的结构:
然而,这个结尾有点特别。因为闭包函数可以访问外函数中的变量,所以它的作用域活动对象在外函数执行后不会被释放(注意外函数执行后会破坏执行环境和对应的作用域链),而是会被闭包函数的作用域链引用,在闭包函数被破坏之前不会破坏外函数的作用域活动对象。这就是闭包占用内存的原因。
因此,使用闭包有优点也有缺点。滥用闭包会导致大量内存消耗。
使用闭包还有其他副作用,可以说是bug也可以说不是。不同的商家可能会有不同的看法。
这个副作用是闭包函数只能得到外部函数变量的最终值。
测试代码如下:(这里使用jquery对象)。
/*闭包缺陷*/(函数($) {var result=newarray(),I=0;for(;i10i ){结果[i]=函数(){返回I;};} $.RES1=结果;})(jQuery);//在数组中执行函数$ . res1[0]();上面的代码首先通过匿名函数表达式打开一个私有范围。这个匿名函数是我们上面提到的外部函数。这个外部函数的参数为$,同时定义了变量result和I。通过for循环为数组结果分配了一个匿名函数。这个匿名函数是一个闭包。他访问了外部函数的变量I。理论上,数组结果会返回对应的数组下标值,但实际情况并不如预期。
上面代码$.RES10的执行结果是10。
为什么呢?因为I的最终值是10。
让我们用下图详细解释一下当执行上述代码时,内存中发生了什么:
那么有没有办法修复这种副作用呢?当然可以!
我们可以通过以下代码实现我们的期望。
/*修复闭包缺陷*/(函数($) {var result=newarray(),I=0;for(;i10I){ result[I]=function(num){ return function(){ return num;} }(I);} $.RES2=结果;})(jQuery);//调用闭包函数console . log($ . res2[0]());内存中的上述代码怎么了?我们也用下图来详细解释。看完上图,我们不难理解下图。
6.简单的例子。
首先,从一个经典的错误开始,页面上有几个div,我们想用onclick方法绑定它们,所以我们有下面的代码。
div id=' div test ' span 0/span span 1/span span 2/span span 3/span/div div id=' div test 2 ' span 0/span span 1/span span 2/span span 3/span/div $(文档)。ready(function(){ var spans=$(' # DivTest span ');for(var I=0;长度;i ) { spans[i]。onclick=function(){ alert(I);} }});这是一个简单的函数,但它就是出错了。每次alert的值为4时,简单的修改就可以了。
var span S2=$(' # div test2 span ');$(文档)。ready(function(){ for(var I=0;I跨度2 .长度;i ) {(函数(num) { spans2[i])。onclick=function(){ alert(num);} })(I);}});7.内部功能。
我们先从一些基础知识开始,首先了解内部功能。内部函数是在另一个函数中定义的函数。例如:
函数outfn(){ functioninnerfn(){ } } innerfn是包装在outfn范围内的内部函数。这意味着在外部内部调用innerFn是有效的,而在外部外部调用innerFn是无效的。以下代码将导致一个JavaScript错误:
函数Outer fn(){ document . write(' Outer function br/');function Inner fn(){ document . write(' Inner function br/');} } innerFn();//unlightreference错误:未定义inner fn,但如果在outerFn内部调用innerFn,它可以成功运行:
函数Outer fn(){ document . write(' Outer function br/');function Inner fn(){ document . write(' Inner function br/');} innerFn();} outfn();8.极大的逃避(内部函数如何逃避外部函数)。
JavaScript开发人员可以像传递任何类型的数据一样传递函数,也就是说,JavaScript中的内部函数可以转义定义它们的外部函数。
有许多方法可以转义,例如,您可以将内部函数分配给全局变量:
//定义全局变量来转义var globalVar函数Outer fn(){ document . write(' Outer function br/');function Inner fn(){ document . write(' Inner function br/');} globalVar=innerFn} outfn();//外部函数内部函数全局变量();//外部函数内部函数innerfn();//referenceerror : innerFn在调用outerFn时没有定义,全局变量globalVar会被修改,此时其引用会变成innerFn,然后调用globalVar就和调用innerFn一样了。此时,直接在outfn之外调用innerFn仍然会导致错误,因为虽然内部函数已经通过将引用保存在全局变量中进行了转义,但该函数的名称仍然只存在于outfn的范围内。
内部函数引用也可以通过在父函数中返回值来获得。
函数Outer fn(){ document . write(' Outer function br/');function Inner fn(){ document . write(' Inner function br/');}返回innerFn} var fnRef=Exterfn();fnRef();外部函数内部不修改全局变量,但是从外部函数返回对内部函数的引用。引用可以通过调用outerFn获得,引用可以保存在变量中。
内部函数即使离开了函数的范围,仍然可以通过引用来调用,这意味着只要有调用内部函数的可能性,JavaScript就需要保留被引用的函数。而且,JavaScript运行时需要跟踪所有引用这个内部函数的变量,JavaScript的垃圾收集器在最后一个变量被丢弃之前无法释放相应的内存空间(红色部分是理解闭包的关键)。
聊了很久,跟闭包有关系。闭包指的是可以访问另一个函数范围内的变量的函数。创建闭包的常见方法是在函数内部创建另一个函数,这就是我们上面提到的内部函数。所以我刚才说的不是废话,也和闭包有关。
9.变量的范围。
内部函数也可以有自己的变量,这些变量仅限于内部函数的范围:
函数Outer fn(){ document . write(' Outer function br/');函数innerFn(){ var innerVar=0;innerVardocument.write('内部函数 t ');document . write(' innerVar=' innerVar ' br/');}返回innerFn} var fnRef=Exterfn();fnRef();fnRef();var fnRef2=外套fn();fnre F2();fnre F2();每次通过引用或其他方式调用这个内部函数时,都会创建一个新的innerVar变量,然后添加1并最终显示。
外部函数内部函数内部var=1内部函数内部var=1外部函数内部函数内部var=1内部函数内部var=1内部函数内部var=1内部函数内部var=1内部函数也可以像其他函数一样引用全局变量:
var global var=0;函数Outer fn(){ document . write(' Outer function br/');函数innerFn(){ global var;document.write('内部函数 t ');document . write(' global var=' global var ' br/');}返回innerFn} var fnRef=Exterfn();fnRef();fnRef();var fnRef2=外套fn();fnre F2();fnre F2();现在,对内部函数的每次调用都会不断增加这个全局变量的值:
外部函数内部函数全局var=1内部函数全局var=2外部函数内部函数全局var=3内部函数全局var=4但是如果这个变量是父函数的局部变量呢?因为内部函数会引用父函数的作用域(如果感兴趣可以了解作用域链和活动对象),内部函数也可以引用这些变量。
函数outfn(){ var outvar=0;document . write(' Outer function br/');函数innerFn(){ ExterVar;document.write('内部函数 t ');document . write(' outvar=' outvar ' br/');}返回innerFn} var fnRef=Exterfn();fnRef();fnRef();var fnRef2=外套fn();fnre F2();fnre F2();这次的结果很有意思,可能超出了我们的预期。
外函数内函数外套Var=1内函数外套Var=2外函数内函数外套var=1内函数外套var=2我们看到的是前两种情况的组合效果,通过每个引用调用innerFn会独立递增外套var。也就是说,对outerFn的第二次调用没有继续使用outwar的值,而是在第二次函数调用的范围内创建并绑定了一个新的outwar实例,两个计数器完全不相关。
当在定义内部函数的范围之外引用内部函数时,会创建内部函数的闭包。在这种情况下,我们把既不是局部变量也不是内部函数参数的变量称为自由变量,把外部函数的环境称为封闭环境。本质上,如果内部函数引用位于外部函数中的变量,就相当于授权该变量被延迟。因此,当外部函数调用完成时,这些变量的内存不会被释放(最后一个值将被保存),闭包仍然需要使用它们。
10.闭包之间的相互作用。
当有多个内部函数时,可能会出现意外的闭包。我们定义一个增量为2的增量函数。
函数outfn(){ var outvar=0;document . write(' Outer function br/');函数innerfn 1(){ outer var;document.write('内部函数1 t ');document . write(' outvar=' outvar ' br/');}函数innerfn 2(){ ExterVar=2;document.write('内部函数2 t ');document . write(' outvar=' outvar ' br/');}返回{ 'fn1': innerFn1,' fn2 ' : innerFn2 };} var fnRef=Exterfn();fnref . fn1();fnref . fn2();fnref . fn1();var fnRef2=外套fn();fnre F2 . fn1();fnre F2 . fn2();fnre F2 . fn1();我们的映射返回两个内部函数的引用,任何一个内部函数都可以被返回的引用调用。结果是:
外部函数内部函数1外部var=1内部函数2外部var=3内部函数1外部var=4外部函数内部函数1外部var=1内部函数2外部var=3内部函数1外部var=4内部fn 1和内部Fn2引用同一个局部变量,因此它们共享一个封闭的环境。当innerFn1将外部Var增加1时,长期丢失的innerFn2将设置外部Var的新起始值,反之亦然。我们还看到,对outerFn的后续调用也将创建这些闭包的新实例和新的封闭环境,本质上是创建一个新的对象。free variables是这个对象的实例变量,而closures是这个对象的实例方法,这些变量也是私有的,因为它们不能在封装它们的范围之外被直接引用,从而保证了面向对象数据的排他性。
11.现在我们可以回顾一下开头写的例子,很容易理解为什么第一种写作方式总是alert 4。
for(var I=0;长度;i ) { spans[i]。onclick=function(){ alert(I);}}上述代码将在页面加载后执行。当I的值为4时,判断条件无效,执行for循环。但是由于此时每个span的onclick方法都是内部函数,所以我被闭包引用了(闭包引用了一个引用),不能破坏内存。I的值将保持为4,直到程序更改它或者所有onclick函数都被销毁(函数被赋为null或者页面被主动卸载)。这样,我们每次点击span,onclick函数都会查找I的值(范围链是引用的方式),检查后等于4,然后会给我们预警。
第二种方法是使用一个可以立即执行的函数,并创建一层闭包。将函数声明放在括号中时,它就变成了表达式,然后通过添加括号来调用它。此时,I作为参数传入,函数立即执行。num每次都保存I的值。
我相信每个人都知道一些关于闭包的知识,就像我一样。当然,如果你完全理解,就需要明确函数的执行环境和范围链。