1.闭包的概念
闭包是指有权访问另一个函数作用域中的变量的函数(注意:闭包是个函数),创建闭包的常见方式是在一个函数内部创建另一个函数(ps:闭包不等于匿名函数,凡是在一个函数内部创建的函数并且内部引用到了外部函数的变量的函数都可以称为闭包,实际上,javascript内的所有函数都是闭包,因为都有作用域链)
2.闭包的现象
先上一个例子:
function createFunction(name){ var newFun=function(){ alert(name); } return newFun; //返回一个闭包 } var resultFun=createFunction('xiaosi2'); resultFun();
在这个例子中,createFunction创建了一个函数newFun并且return回来,这个newFun就是一个闭包(注意newFun不是匿名函数)。而resultFun就是createFunction返回的那个函数,我们看到参数‘xiaosi2’是在createFunction这个函数中传入的,但是确是在resultFun这个函数执行的时候显示出来,说明了一个现象——resultFun可以访问createFunction的变量,即:使用闭包可以访问另一个函数的变量。但这同时也引起另一个问题——createFunction执行完了之后是需要被垃圾回收机制回收相应的内存空间的,并且‘xiaosi2’这个变量并没有在执行createFunction的时候变成全局变量,resultFun怎么能访问到?
ps:
1.newFun内用了name是createFunction的参数,如果newFun也有一个同名参数name,即
var newFun=function(name){}的话,newFun就不是闭包了,因为内部用到的参数不是外层作用域链的对象(即不是外层参数)
2.不是resultFun是闭包,而是newFun,resultFun不过是指向newFun罢了,即使newFun没有return出来,它也是闭包,因为可以访问到createFunction的变量,但是这样newFun会直接被垃圾回收掉,因为没有“用武之地”,显然这样的闭包对我们没什么用处
3.闭包的本质
我们刚才提到了,resultFun可以访问到createFunction内的变量是一件很奇怪的事情,实际上也没有很特别,不过是把resultFun内用到的createFunction内的变量保存下来了而已——保存用到的函数作用域链上用的对象(和原型链继承有些类似)
在函数的属性内有一个[[scope]]属性(这个属性在函数创建的时候就会存在,直至函数被销毁),保存的是scope chain列表,scope=AO(活动对象)+[[scope]],普通函数的作用域链只有2级,本身的参数变量(AO)和全局变量([[scope]]带来的,全局VA常驻内存),然而闭包函数因为引用到了其他函数(通常就是父级或者更上层)的变量,在作用域链内就会存储这个变量对象(注意,是用到什么才会存储什么,用不到的依旧会被销毁,在[[scope]]中被销毁的),这样就实现了newFun可以访问createFunction的变量,然而这并不是resultFun能访问createFunction的全部原因,因为后面又把resultFun指向newFun,这样就造成了对newFun的引用,从而垃圾回收不会回收newFun,所以我们就可以访问到没有被销毁的变量对象,也就是可以访问到‘xiaosi2’的原因
值得一提的是,函数是共享父级的[[scope]]的,所以一个函数对一个父级的变量更改的话会导致所有的函数访问到的,这也就是为什么没有引用到的变量会被销毁,因为在父级的函数内用不到就被销毁了,而子函数只是指向父函数AO而已
4.如何使用闭包
有趣的是,一般我们会使用狭义的“匿名函数闭包”的情况都是我们使用了另一个闭包的原因导致的。比如说for循环的执行事件:
function createFunction() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = function() { console.dir(arguments.callee); return i; } } return result; } var newFun = createFunction(); for (var i = 0, max = newFun.length; i < max; i++) { console.log(newFun[i]()); //10 }
这样会导致循环十次的结果都是10,因为内部的函数在定义的时候就是添加作用域链,[[scope]]就已经指向了父函数的AO,我们循环的时候导致父函数的AO.i=10了(AO不是真实对象,是抽象概念,我这么写只是为了方便),然后再执行子函数的时候,会在[[scope]]内直接找到i,而这时i的值已经为10了。而解决的办法就是再加一层闭包:
function createFunction() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] =(function(num) { console.dir(arguments.callee); return function(){ return num; }; })(i); } return result; } var newFun = createFunction(); for (var i = 0, max = newFun.length; i < max; i++) { console.log(newFun[i]()); //10 }
这样实际上并不是我们狭义的“匿名函数闭包解决参数共享问题”,而是通过多加一层闭包,使得每一个num指向的不是createFunction的AO而是新建的匿名函数的AO,而每个匿名函数的AO又是不一样的(因为我们建立了10个函数),所以就不会共享了,单很明显的,消耗的内存高很多,恩,基于这个原理,其实我们把匿名函数变成有名字的函数是一样的:
function xiaosi(num) { console.dir(arguments.callee); return function() { return num; }; } function createFunction() { var result = new Array(); for (var i = 0; i < 10; i++) { result[i] = xiaosi(i); } return result; } var newFun = createFunction(); for (var i = 0, max = newFun.length; i < max; i++) { console.log(newFun[i]()); //10 }
在这里,我们调用了另一个全局函数,并且i作为参数传入,这样就切断了返回值和循环i之间的直接联系,num作为xiaosi本身的AO而不是[[scope]]从而使得每次调用xiaosi的时候会覆盖掉以前的值,所以也可以解决问题,如果是声明内部函数也是可行的,区别就是把全局函数变为“闭包函数”而已
function createFunction() { var result = new Array(); for (var i = 0; i < 10; i++) { function xiaosi(num) { return function() { return num; } } result[i] = xiaosi(i); } return result; } var newFun = createFunction(); for (var i = 0, max = newFun.length; i < max; i++) { console.log(newFun[i]()); //10 }