闭包----你所不知道的JavaScript系列(4)
一、闭包是什么?
· 闭包就是可以使得函数外部的对象能够获取函数内部的信息。
· 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
· 闭包就是一个“捕获”或“携带”了其被生成的环境中、所属的变量范围内所引用的所有变量的函数。
还有很多很多解释......
函数对象可以通过作用域链互相关联起来,函数体内部的变量都可以保存在函数作用域内,这叫做“闭包”。 --《JavaScript权威指南》
当函数可以记住并访问所在的词法作用域时, 就产生了闭包, 即使函数是在当前词法作用域之外执行。 --《你所不知道的JavaScript》
function foo() { var a = 2; function bar() { console.log( a ); } return bar; } var baz = foo(); baz(); // 2
这就是闭包的效果。 函数 bar() 的词法作用域能够访问 foo() 的内部作用域。然后我们将 bar() 函数本身当作一个值类型进行传递。在这个例子中,我们将 bar 所引用的函数对象本身当作返回值。bar()显然可以被正常执行。但是在这个例子中,它在自己定义的词法作用域以外的地方执行。在 foo() 执行后, 通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收器用来释放不再使用的内存空间。由于看上去 foo() 的内容不会再被使用,所以很自然地会考虑对其进行回收。而闭包的“神奇” 之处正是可以阻止这件事情的发生。事实上内部作用域依然存在,因此没有被回收。 谁在使用这个内部作用域? 原来是 bar() 本身在使用。拜 bar() 所声明的位置所赐,它拥有涵盖 foo() 内部作用域的闭包,使得该作用域能够一直存活,以供 bar() 在之后任何时间进行引用。bar() 依然持有对该作用域的引用, 而这个引用就叫作闭包。
无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都可以观察到闭包。闭包使得函数可以继续访问定义时的词法作用域。
二、作用域链和js垃圾回收机制
在深入理解闭包之前,最好能先理解一下作用域链的含义以及js垃圾回收机制。
简单来说,作用域链就是函数在定义的时候创建的(而不是在函数调用时定义),用于寻找使用到的变量的值的一个索引,而他内部的规则是,把函数自身的本地变量放在最前面,把自身的父级函数中的变量放在其次,把再高一级函数中的变量放在更后面,以此类推直至全局对象为止。当函数中需要查询一个变量的值的时候,js解释器会去作用域链去查找,从最前面的本地变量中先找,如果没有找到对应的变量,则到下一级的链上找,一旦找到了变量,则不再继续。如果找到最后也没找到需要的变量,则解释器返回undefined。
了解了作用域链,我们再来看看js的内存回收机制。一般来说,一个函数在执行开始的时候,会给其中定义的变量划分内存空间保存,以备后面的语句所用,等到函数执行完毕返回了,这些变量就被认为是无用的了,对应的内存空间也就被回收了。下次再执行此函数的时候,所有的变量又回到最初的状态,重新赋值使用。但是如果这个函数内部又嵌套了另一个函数,而这个函数是有可能在外部被调用到的,并且这个内部函数又使用了外部函数的某些变量的话,这种内存回收机制就会出现问题。如果在外部函数返回后,又直接调用了内部函数,那么内部函数就无法读取到他所需要的外部函数中变量的值了。所以js解释器在遇到函数定义的时候,会自动把函数和他可能使用的变量(包括本地变量和父级和祖先级函数的变量(自由变量))一起保存起来。也就是构建一个闭包,这些变量将不会被内存回收器所回收,只有当内部的函数不可能被调用以后(例如被删除了,或者没有了指针),才会销毁这个闭包,而没有任何一个闭包引用的变量才会被下一次内存回收启动时所回收。
三、闭包的缺点以及优点
缺点:
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
优点:
(1)希望一个变量长期驻扎在内存中。
(2)避免全局变量的污染。
(3)私有成员的存在。
接下来,我们就针对闭包的三个优点进行解析,一起来看看吧。
储存变量
function test() { var a = 1; return function(){ alert(a++) }; } var fun = test(); fun(); // 1 执行后 a++,然后a还在~ fun(); // 2 fun = null; //解除引用,等待垃圾回收
在执行fun = test()时,其实就相当于fun = function(){ alert(a++) }。如果我们平时这样声明并赋值一个变量的时候,在调用的时候就会报错(a未被声明)。但是在闭包中却不会报错,因为在调用test() 的时候,变量a是存在于内部匿名函数的作用域链上的,并且在调用完之后变量a不会消失。
原因:由于匿名函数一直在引用变量a,js垃圾回收机制将不会把变量a当做垃圾去回收,所以a会一直存在内存当中。
既然知道了闭包能将变量一直保存在内存中,那么我们再来看看一下几个例子的区别,来加深对闭包的了解。
第一段代码:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); //返回值会是什么?
第二段代码:
var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); //返回值会是什么?
我们可以看到第一段代码,在函数checkscope内返回的函数f的结果,很清楚可以知道返回值为"local scope"。而在第二段代码中,函数checkscope内返回的函数对象f,返回的是一个对象而不是一个运算的结果。现在在定义函数的作用域外面,调用这个嵌套函数f,返回的结果依旧是"local scope"。为什么在定义函数外部调用嵌套函数,返回的结果却还是函数内部变量。这里面就涉及到了函数的作用域链。上面说过,函数的作用域链是在函数定义时就生成的,而不是在函数调用时生成的。嵌套函数f()定义时在checkscope的作用域链内,其中的scope是局部变量,其值为local scope,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效,因此返回的值是"local scope"。
避免全局污染
在这里,我先提个问题,如果要让你实现一个从数字1开始累加的功能,你会怎么实现?
我们来看看下面两个代码。
//使用全局变量 var a = 1; function abc(){ a++; alert(a); } abc(); //2 abc(); //3
//使用局部变量 function abc(){ var a = 1; a++; alert(a); } abc(); //2 abc(); //2
在上面的例子可以看出,使用全局变量可以很轻松就实现累加效果,但是使用全局变量会造成全局污染。我们可以使用局部变量来实现累加,但是上面使用局部变量的结果不尽人意(原因:只是对函数进行简单的调用,每次调用完之后函数内的变量都会被销毁。再次调用时会重新赋值初值,并不会保存上一次调用后的值)。那我们要怎么才能利用局部变量实现累加呢?
在前面的例子中,我们看到了闭包可以保存函数内的变量,所以我们可以利用闭包将变量a的值保存起来,每次调用之后,a的值会累加并且不会被销毁。来看看下面怎么利用闭包怎么实现累加功能的。
function outer(){ var x=1; return function(){ x++; alert(x); } } var y = outer(); //外部函数赋给变量y y(); //y函数调用一次,结果为2,相当于outer()() y(); //y函数调用第二次,结果为3,实现了累加
我们知道,js是没有块级作用域的概念的,这里我们就可以看到,可以用闭包来模拟模拟块级作用域,从而避免全局污染。
私有成员
由于闭包可以捕捉到单个函数调用的局部变量,并将这些局部变量用做私有状态,所以可以来定义私有成员。
var init = (function(){ var counter = 0; return function(){ return counter++; } })(); init(); //0 init(); //1 init(); //2
上面这段代码定义了一个立即调用的函数,因此这个函数的返回值赋值给了变量init。再来看一下函数体,这个函数返回另外一个函数,由于被返回的函数能够访问自己作用域内的变量,而且能够访问其外部函数(函数体)中定义的counter变量。当立即执行函数执行之后,其他任何代码都无法访问变量counter,只有其内部的函数才能访问到它。所以此时变量counter就变成一个私有变量,存在于闭包中。
像counter这样的私有变量不单只可以存在一个单独的闭包中,在同一个外部函数内定义的多个嵌套函数也可以访问到它,这多个嵌套函数都共享一个作用域链,现在看一下这段代码。
function counter(){ var n = 0; return{ count : function(){ return n++; } reset : function(){ n =0; } } } var c = counter(), d = counter(); c.count(); //0 d.count(); //0 c.reset(); //reset()和count()方法共享 c.count(); //0 (因为重置了c) d.count(); //1 (没有重置d,因此n继续累加)
如果现在需要实现一个功能:返回一个函数组成的数组,并且它们的返回值分别是0~9。你会怎么实现?会不会跟下面一段代码一样?
function contsfuncs(){ var funcs = []; for(var i = 0; i<10; i++){ funcs[i] = function(){ return i; } } return funcs; } var funcs = contsfuncs(); funcs[5](); //返回值是什么?
上面这段代码创建了10个闭包,并且将它们存储到一个数组中。由于这些闭包都是在同一个函数调用中定义的,所以他们都可以共享变量i。当contsfuncs()返回时,变量i都是10,所以所有的闭包都共享这一个变量值,因此,数组中的函数的返回值都是同一个值。那我们要怎么做才能实现我们想要的功能呢?
function contsfuncs(v){ return function(){ return v; }; } var funcs = []; for(var i = 0; i<10; i++){ funcs[i] = contsfuncs(i); } funcs[5](); //5
由于外部函数contsfuncs()总是返回一个返回变量v的值,所以在for循环中,由于每次调用外部函数contsfuncs()时,传入的v的值是不同的,所以所形成的闭包都是不同的,这十个闭包中变量v的值分别为0~9,所以数组funcs中在第五个位置的元素所表示的函数返回值为5。
总结:
(1)在同一个调用函数内部定义多个闭包,这些闭包共享调用函数的变量,每个闭包对其操作都会影响到其他闭包对其引用的值。
(2)利用同一个调用函数在函数外部构造的多个闭包,则这些闭包都是独立的,拥有自己的作用域链,互不干扰。
四、闭包中的this
在闭包中使用this,需要特别小心,因为很容易就出错。不信?看看下面例子就知道了。
var name = "window"; var obj = { name : "object", getName : function(){ return function(){ return this.name; } } }; alert(obj.getName()()); // window
我们本来的想法是想调用闭包后返回obj对象中的name的值,即"object",但是结果却是返回"window"。为什么这个闭包返回的this.name的值不是局部变量name的值,而是全局变量name的值?
在这里首先必须要说的是,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象。this是JavaScript的关键字,而不是变量,每个函数调用都包含一个this的值,如果闭包在外部函数里是无法访问到闭包里面的this值的。因为这个this和当初定义函数时的this不是同一个,即便是同一个this,this的值是随着调用栈的变化而变化的,而闭包里的逻辑所取到的this的值也不是确定的。由于匿名函数的执行具有全局性,因此其this通常指向window。当然,我们还是有办法来解决这种问题的,就是将this转存为一个变量就可以避免this的不确定性带来的歧义。如下:
var name = "window"; var obj = { name : "object", getName : function(){ var that = this; return function(){ return that.name; } } }; alert(obj.getName()()); // object
五、小试牛刀
看看点击不同li标签时,alert的值会是多少?
HTML:
<ul> <li>0</li> <li>1</li> <li>2</li> <li>3</li> </ul>
JS:
window.onload = function(){ var aLi = document.getElementsByTagName('li'); for (var i=0;i<aLi.length;i++){ aLi[i].onclick = function(){ alert(i); }; }
这是一道很经典的笔试题,也是很多初学者经常犯错而且找不到原因的一段代码。想要实现的效果是点击不同的<li>标签,alert出其对应的索引值,但是实际上代码运行之后,我们会发现不管点击哪一个<li>标签,alert出的i都为4。为什么呢?因为在执行for循环之后,i的值已经变成了4,等到点击<li>标签时,alert的i值是4。下面就用闭包来解决这个问题。
window.onload = function(){ var aLi = document.getElementsByTagName('li');
for (var i=0;i<aLi.length;i++){
(function(i){
aLi[i].onclick = function(){
alert(i);
};
})(i);
}
}
你做对了吗?