用JavaScript一年多了,闭包老是让人二丈和尚摸不着头脑。陆陆续续接触了一些闭包的知识,也犯过几回由于不理解闭包致使的错误,一年多了资料也看了一些,但仍是不是很是明白,最近偶然看了一下 jQuery基础教程 的附录,发现附录A对JavaScript的闭包的介绍简单易懂,因而借花献佛总结一下。javascript
闭包:是指有权访问另一个函数做用域中的变量的函数。建立闭包的常见方式就是在一个函数内部建立另一个函数。html
直接上例子java
function a(){
var i=0;
function b(){
alert(++i);
}
return b;
}
var c = a();
c();
这段代码有两个特色:jquery
一、函数b嵌套在函数a内部;
二、函数a返回函数b。数组
这样在执行完var c=a()后,变量c其实是指向了函数b,再执行c()后就会弹出一个窗口显示i的值(第一次为1)。这段代码其实就建立了一个闭包,为何?由于函数a外的变量c引用了函数a内的函数b,就是说:
当函数a的内部函数b被函数a外的一个变量引用的时候,就建立了一个闭包。安全
我猜测你必定仍是不理解闭包,由于你不知道闭包有什么做用,下面让咱们继续探索。markdown
简而言之,闭包的做用就是在a执行完并返回后,闭包使得Javascript的垃圾回收机制GC不会收回a所占用的资源,由于a的内部函数b的执行须要依赖a中的变量。这是对闭包做用的很是直白的描述,不专业也不严谨,但大概意思就是这样,理解闭包须要按部就班的过程。
在上面的例子中,因为闭包的存在使得函数a返回后,a中的i始终存在,这样每次执行c(),i都是自加1后alert出i的值。闭包
那么咱们来想象另外一种状况,若是a返回的不是函数b,状况就彻底不一样了。由于a执行完后,b没有被返回给a的外界,只是被a所引用,而此时a也只会被b引用,所以函数a和b互相引用但又不被外界打扰(被外界引用),函数a和b就会被GC回收。(关于Javascript的垃圾回收机制将在后面详细介绍)app
若是要更加深刻的了解闭包以及函数a和嵌套函数b的关系,咱们须要引入另外几个概念:函数的执行环境(excution context)、活动对象(call object)、做用域(scope)、做用域链(scope chain)。以函数a从定义到执行的过程为例阐述这几个概念。函数
一、当定义函数a的时候,js解释器会将函数a的做用域链(scope chain)设置为定义a时a所在的“环境”,若是a是一个全局函数,则scope chain中只有window对象。
二、当函数a执行的时候,a会进入相应的执行环境(excution context)。
三、在建立执行环境的过程当中,首先会为a添加一个scope属性,即a的做用域,其值就为第1步中的scope chain。即a.scope=a的做用域链。
四、而后执行环境会建立一个活动对象(call object)。活动对象也是一个拥有属性的对象,但它不具备原型并且不能经过JavaScript代码直接访问。建立完活动对象后,把活动对象添加到a的做用域链的最顶端。此时a的做用域链包含了两个对象:a的活动对象和window对象。
五、下一步是在活动对象上添加一个arguments属性,它保存着调用函数a时所传递的参数。
六、最后把全部函数a的形参和内部的函数b的引用也添加到a的活动对象上。在这一步中,完成了函数b的的定义,所以如同第3步,函数b的做用域链被设置为b所被定义的环境,即a的做用域。
到此,整个函数a从定义到执行的步骤就完成了。此时a返回函数b的引用给c,又函数b的做用域链包含了对函数a的活动对象的引用,也就是说b能够访问到a中定义的全部变量和函数。函数b被c引用,函数b又依赖函数a,所以函数a在返回后不会被GC回收。
当函数b执行的时候亦会像以上步骤同样。所以,执行时b的做用域链包含了3个对象:b的活动对象、a的活动对象和window对象,当在函数b中访问一个变量的时候,搜索顺序是先搜索自身的活动对象,若是存在则返回,若是不存在将继续搜索函数a的活动对象,依次查找,直到找到为止。若是整个做用域链上都没法找到,则返回undefined。若是函数b存在prototype原型对象,则在查找完自身的活动对象后先查找自身的原型对象,再继续查找。这就是Javascript中的变量查找机制。
一、保护函数内的变量安全。以最开始的例子为例,函数a中i只有函数b才能访问,而没法经过其余途径访问到,所以保护了i的安全性。
二、在内存中维持一个变量。依然如前例,因为闭包,函数a中i的一直存在于内存中,所以每次执行c(),都会给i自加1。
以上两点是闭包最基本的应用场景,不少经典案例都源于此。
在Javascript中,若是一个对象再也不被引用,那么这个对象就会被GC回收。若是两个对象互相引用,而再也不被第3者所引用,那么这两个互相引用的对象也会被回收。由于函数a被b引用,b又被a外的c引用,这就是为何函数a执行后不会被回收的缘由。
在javascript中没有块级做用域,通常为了给某个函数申明一些只有该函数才能使用的局部变量时,咱们就会用到闭包,这样咱们能够很大程度上减小全局做用域中的变量,净化全局做用域。
使用闭包有如上的好处,固然这样的好处是须要付出代价的,代价就是内存的占用。
如何理解上面的那句话呢?
每一个函数的执行,都会建立一个与该函数相关的函数执行环境,或者说是函数执行上下文。这个执行上下文中有一个属性 scope chain(做用域链指针),这个指针指向一个做用域链结构,做用域链中的指针又都指向各个做用域对应的活动对象。正常状况,一个函数在调用开始执行时建立这个函数执行上下文及相应的做用域链,在函数执行结束后释放函数执行上下文及相应做用域链所占的空间。
先引用–John Hou的几张图http://www.cnblogs.com/johnhou/p/javascript.html
//声明函数
function test(){
var str = "hello world";
console.log(str);
}
//调用函数
test();
在调用函数的时候会在内存中生成以下图的结构:
可是闭包的状况就有点特殊了,因为闭包函数能够访问外层函数中的变量,因此外层函数在执行结束后,其做用域活动对象并不会被释放(注意,外层函数执行结束后执行环境和对应的做用域链就会被销毁),而是被闭包函数的做用域链所引用,直到闭包函数被销毁后,外层函数的做用域活动对象才会被销毁。这也正是闭包要占用内存的缘由。
因此使用闭包有好处,也有坏处,滥用闭包会形成内存的大量消耗。
使用闭包还有其余的反作用,能够说是bug,也能够说不是,相对不一样的业务可能就会有不一样的见解。
这个反作用是闭包函数只能取到外层函数变量的最终值。
测试代码以下:(这里使用了jquery对象)
/*闭包缺陷*/
(function($){
var result = new Array(),
i = 0;
for(;i<10;i++){
result[i] = function(){
return i;
};
}
$.RES1 = result;
})(jQuery);
// 执行数组中的函数
$.RES1[0]();
上面的代码先经过匿名函数表达式开辟了一块私有做用域,这个匿名函数就是咱们上面所说的外层函数,该外层函数有一个参数$,同时还定义了变量result和 I , 经过for循环给数组result赋值一个匿名函数,这个匿名函数就是闭包,他访问了外层函数的变量I , 理论上数组resulti 会返回相应的数组下标值,实际状况却不如所愿。
如上代码 $.RES10 的执行结果是10.
为何会这样呢,由于i的最终值就是10.
下面咱们经过下图来详细说明下,上面的那段代码执行时在内存中到底发生了什么:
那么这个反作用有没有办法能够修复呢?固然能够!
咱们能够经过下面的代码来达到咱们的预期。
/*修复闭包缺陷*/
(function($){
var result = new Array(),
i = 0;
for(;i<10;i++){
result[i] = function(num){
return function(){
return num;
}
}(i);
}
$.RES2 = result;
})(jQuery);
//调用闭包函数
console.log($.RES2[0]());
上面的代码又在内存中发生了什么?咱们一样用下面的一幅图来详细解释。看懂了上面的图,咱们也就不难理解下面的图。
首先从一个经典错误谈起,页面上有若干个div, 咱们想给它们绑定一个onclick方法,因而有了下面的代码
<div id="divTest">
<span>0</span> <span>1</span> <span>2</span> <span>3</span>
</div>
<div id="divTest2">
<span>0</span> <span>1</span> <span>2</span> <span>3</span>
</div>
$(document).ready(function() {
var spans = $("#divTest span");
for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); } } });
很简单的功能但是却恰恰出错了,每次alert出的值都是4,简单的修改就好使了
var spans2 = $("#divTest2 span");
$(document).ready(function() {
for (var i = 0; i < spans2.length; i++) {
(function(num) {
spans2[i].onclick = function() {
alert(num);
}
})(i);
}
});
让咱们从一些基础的知识谈起,首先了解一下内部函数。内部函数就是定义在另外一个函数中的函数。例如:
function outerFn () {
functioninnerFn () {}
}
innerFn就是一个被包在outerFn做用域中的内部函数。这意味着,在outerFn内部调用innerFn是有效的,而在outerFn外部调用innerFn则是无效的。下面代码会致使一个JavaScript错误:
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
document.write("Inner function<br/>");
}
}
innerFn();//Uncaught ReferenceError: innerFn is not defined
不过在outerFn内部调用innerFn,则能够成功运行:
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
document.write("Inner function<br/>");
}
innerFn();
}
outerFn();
JavaScript容许开发人员像传递任何类型的数据同样传递函数,也就是说,JavaScript中的内部函数可以逃脱定义他们的外部函数。
逃脱的方式有不少种,例如能够将内部函数指定给一个全局变量:
//定义全局变量逃脱
var globalVar;
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
document.write("Inner function<br/>");
}
globalVar = innerFn;
}
outerFn(); //Outer function Inner function globalVar(); //Outer function Inner function innerFn(); //ReferenceError: innerFn is not defined
调用outerFn时会修改全局变量globalVar,这时候它的引用变为innerFn,此后调用globalVar和调用innerFn同样。这时在outerFn外部直接调用innerFn仍然会致使错误,这是由于内部函数虽然经过把引用保存在全局变量中实现了逃脱,但这个函数的名字依然只存在于outerFn的做用域中。
也能够经过在父函数的返回值来得到内部函数引用
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
document.write("Inner function<br/>");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
这里并无在outerFn内部修改全局变量,而是从outerFn中返回了一个对innerFn的引用。经过调用outerFn可以得到这个引用,并且这个引用能够能够保存在变量中。
这种即便离开函数做用域的状况下仍然可以经过引用调用内部函数的事实,意味着只要存在调用内部函数的可能,JavaScript就须要保留被引用的函数。并且JavaScript运行时须要跟踪引用这个内部函数的全部变量,直到最后一个变量废弃,JavaScript的垃圾收集器才能释放相应的内存空间(红色部分是理解闭包的关键)。
说了半天总算和闭包有关系了,闭包是指有权限访问另外一个函数做用域的变量的函数,建立闭包的常见方式就是在一个函数内部建立另外一个函数,就是咱们上面说的内部函数,因此刚才说的不是废话,也是闭包相关的 ^_^
内部函数也能够有本身的变量,这些变量都被限制在内部函数的做用域中:
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
var innerVar = 0;
innerVar++;
document.write("Inner function\t");
document.write("innerVar = "+innerVar+"<br/>");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
每当经过引用或其它方式调用这个内部函数时,就会建立一个新的innerVar变量,而后加1,最后显示
Outer function
Inner function innerVar = 1
Inner function innerVar = 1
Outer function
Inner function innerVar = 1
Inner function innerVar = 1
内部函数也能够像其余函数同样引用全局变量:
var globalVar = 0;
function outerFn() {
document.write("Outer function<br/>");
function innerFn() {
globalVar++;
document.write("Inner function\t");
document.write("globalVar = " + globalVar + "<br/>");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
如今每次调用内部函数都会持续地递增这个全局变量的值:
Outer function
Inner function globalVar = 1
Inner function globalVar = 2
Outer function
Inner function globalVar = 3
Inner function globalVar = 4
可是若是这个变量是父函数的局部变量又会怎样呢?由于内部函数会引用到父函数的做用域(有兴趣能够了解一下做用域链和活动对象的知识),内部函数也能够引用到这些变量
function outerFn() {
var outerVar = 0;
document.write("Outer function<br/>");
function innerFn() {
outerVar++;
document.write("Inner function\t");
document.write("outerVar = " + outerVar + "<br/>");
}
return innerFn;
}
var fnRef = outerFn();
fnRef();
fnRef();
var fnRef2 = outerFn();
fnRef2();
fnRef2();
这一次结果很是有意思,也许或出乎咱们的意料
Outer function
Inner function outerVar = 1
Inner function outerVar = 2
Outer function
Inner function outerVar = 1
Inner function outerVar = 2
咱们看到的是前面两种状况合成的效果,经过每一个引用调用innerFn都会独立的递增outerVar。也就是说第二次调用outerFn没有继续沿用outerVar的值,而是在第二次函数调用的做用域建立并绑定了一个一个新的outerVar实例,两个计数器彻底无关。
当内部函数在定义它的做用域的外部被引用时,就建立了该内部函数的一个闭包。这种状况下咱们称既不是内部函数局部变量,也不是其参数的变量为自由变量,称外部函数的调用环境为封闭闭包的环境。从本质上讲,若是内部函数引用了位于外部函数中的变量,至关于受权该变量可以被延迟使用。所以,当外部函数调用完成后,这些变量的内存不会被释放(最后的值会保存),闭包仍然须要使用它们。
当存在多个内部函数时,极可能出现意料以外的闭包。咱们定义一个递增函数,这个函数的增量为2
function outerFn() {
var outerVar = 0;
document.write("Outer function<br/>");
function innerFn1() {
outerVar++;
document.write("Inner function 1\t");
document.write("outerVar = " + outerVar + "<br/>");
}
function innerFn2() {
outerVar += 2;
document.write("Inner function 2\t");
document.write("outerVar = " + outerVar + "<br/>");
}
return { "fn1": innerFn1, "fn2": innerFn2 };
}
var fnRef = outerFn();
fnRef.fn1();
fnRef.fn2();
fnRef.fn1();
var fnRef2 = outerFn();
fnRef2.fn1();
fnRef2.fn2();
fnRef2.fn1();
咱们映射返回两个内部函数的引用,能够经过返回的引用调用任一个内部函数,结果:
Outer function
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4
Outer function
Inner function 1 outerVar = 1
Inner function 2 outerVar = 3
Inner function 1 outerVar = 4
innerFn1和innerFn2引用了同一个局部变量,所以他们共享一个封闭环境。当innerFn1为outerVar递增一时,久违innerFn2设置了outerVar的新的起点值,反之亦然。咱们也看到对outerFn的后续调用还会建立这些闭包的新实例,同时也会建立新的封闭环境,本质上是建立了一个新对象,自由变量就是这个对象的实例变量,而闭包就是这个对象的实例方法,并且这些变量也是私有的,由于不能在封装它们的做用域外部直接引用这些变量,从而确保了了面向对象数据的专有性。
如今咱们能够回头看看开头写的例子就很容易明白为何第一种写法每次都会alert 4了。
for (var i = 0; i < spans.length; i++) { spans[i].onclick = function() { alert(i); }
}
上面代码在页面加载后就会执行,当i的值为4的时候,判断条件不成立,for循环执行完毕,可是由于每一个span的onclick方法这时候为内部函数,因此i被闭包引用(闭包引用传的是引用),内存不能被销毁,i的值会一直保持4,直到程序改变它或者全部的onclick函数销毁(主动把函数赋为null或者页面卸载)时才会被回收。这样每次咱们点击span的时候,onclick函数会查找i的值(做用域链是引用方式),一查等于4,而后就alert给咱们了。
而第二种方式是使用了一个当即执行的函数又建立了一层闭包,函数声明放在括号内就变成了表达式,后面再加上括号括号就是调用了,这时候把i当参数传入,函数当即执行,num保存每次i的值。
这一通下来想必你们也和我同样,对闭包有所了解了吧,固然彻底了解的话须要把函数的执行环境和做用域链搞清楚 ^_^
三、你不知道的JavaScript–Item3 隐式强制转换
四、你不知道的JavaScript–Item4 基本类型和基本包装类型(引用类型)
六、你不知道的JavaScript–Item6 var预解析与函数声明提高(hoist )
七、你不知道的JavaScript–Item7 函数和(命名)函数表达式
八、你不知道的JavaScript–Item8 函数,方法,构造函数调用
九、你不知道的JavaScript–Item9 call(),apply(),bind()与回调
十、你不知道的JavaScript–Item10 闭包(closure)
十一、你不知道的JavaScript–Item11 arguments对象
十二、你不知道的JavaScript–Item12 undefined 与 null
1三、你不知道的JavaScript–Item13 理解 prototype, getPrototypeOf 和_ proto_
1四、你不知道的JavaScript–Item14 使用prototype的几点注意事项
1五、你不知道的JavaScript–Item15 prototype原型和原型链详解
1六、你不知道的JavaScript–Item16 for 循环和for…in 循环的那点事儿
1七、你不知道的JavaScript–Item17 循环与prototype最后的几点小tips
1八、你不知道的JavaScript–Item18 JScript的Bug与内存管理
1九、你不知道的JavaScript–Item19 执行上下文(execution context)
20、你不知道的JavaScript–Item20 做用域与做用域链(scope chain)
2一、你不知道的JavaScript–Item21 漂移的this