闭包
1.闭包(Closure):指有权访问另一个函数作用域中的变量的函数。
function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); //999
上面的代码中的f2函数就是闭包。
1 function createComparisonFunction(propertyName) { 2 return function(object1, object2) { 3 var value1 = object1[propertyName); 4 var value2 = object2[propertyName); 5 if(value1 < value2) { 6 return -1; 7 } else if (value1 > value2) { 8 return 1; 9 } else { 10 return 0; 11 } 12 }; 13 }
作用域链对理解闭包至关重要。当某个函数被调用时,会创建一个执行环境(execution context)及相应的作用域链。然后,使用arguments和其他命名参数的值来初始化函数的活动对象(activation object)。但在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位,……直至作为作用域链终点的全局执行环境。
在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。
1 var compare = createComparisonFunction("name"); 2 var result = compare({name: 'Nicholas'}, {name: 'Greg'});
下图展示了当上述代码执行时,包含函数与内部匿名函数的作用域链。
在匿名函数从createComparisonFunction()中被返回后,它的作用域链被初始化为包含createComparisonFunciton()函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunciton()中定义的所有变量。更为重要的是,createComparisonFunciton()函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当createComparisonFunciton()函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直至匿名函数被销毁后,createComparisonFunciton()的活动对象才会被销毁。
1 //创建函数 2 var compareNames = createComparisonFunction('name'); 3 4 //调用函数 5 var result = compareNames({name: "Nicholas"}, {name: "Greg"}); 6 7 //解除对匿名函数的引用(以便释放内存) 8 compareNames = null;
通过将compareNames设置为等于null解除对该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全地销毁了。
2.闭包与变量
作用域链的这种配置机制引出了一个值得注意的副作用,即闭包只能取得包含函数中任何变量的最后一个值。
1 function createFunctions() { 2 var result = new Array(); 3 for(var i = 0; i < 10; i++) { 4 result[i] = function() { 5 return i; 6 }; 7 } 8 return result; //[10,10,10,10,10,10,10,10,10,10] 9 }
这个函数会返回一个函数数组,每个函数都返回10。因为每个函数的作用域链中都保存着createFunctions()函数的活动对象,所以它们引用的都是同一个变量i。当createFunctions()函数返回后,变量i的值是10,此时每个函数都引用着保存变量i的同一个变量对象,所以在每个函数内部i的值都是10。
1 function createFunctions() { 2 var result = new Array(); 3 for(var i=0; i<10; i++) { 4 result[i] = function(num) { 5 return function() { 6 return num; 7 }; 8 }(i); //立即执行函数 9 } 10 return result; 11 }
上述函数执行的结果符合我们的预期。
3.关于this对象
在闭包中使用this对象可能会导致一些问题。this对象是在运行时基于函数的执行环境绑定的:在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过匿名函数的执行环境具有全局性,因此其this对象通常指向window(如果通过call()或apply()改变函数执行环境的情况下,this就会指向其他对象)。
1 var name = "The Window"; 2 3 var object = { 4 name: "My Object", 5 getNameFunc: function() { 6 return function () { 7 return this.name; 8 }; 9 } 10 }; 11 12 alert(object.getNameFunc()()); //"The Window"
为什么匿名函数没有取得其包含作用域(或外部作用域)的this对象呢? 因为,每个函数被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
1 var name = "The Window"; 2 var object = { 3 name: "My Object", 4 getNameFunc: function() { 5 var that = this; 6 return function() { 7 return that.name; 8 }; 9 } 10 }; 11 12 alert(object.getNameFunc()()); //"My Object"
在定义匿名函数之前,把this对象赋值给一个名叫that的变量。而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。
注:this和arguments存在同样的问题。如果想访问作用域中的arguments对象,必须将对该对象的引用保存到另一个闭包能够访问的变量中。
4.闭包之间的交互
1 function outerFn() { 2 var outerVar = 0; 3 function innerFn1() { 4 outerVar++; 5 console.log('(1) outerVar = ' + outerVar); 6 } 7 function innerFn2() { 8 outerVar += 2; 9 console.log('(2) outerVar = ' + outerVar); 10 } 11 return {'fn1': innerFn1, 'fn2': innerFn2}; 12 } 13 14 var fnRef = outerFn(); 15 fnRef.fn1(); 16 fnRef.fn2(); 17 fnRef.fn1(); 18 19 var fnRef2 = outerFn(); 20 fnRef2.fn1(); 21 fnRef2.fn2(); 22 fnRef2.fn1(); 23 24 //(1) outerVar = 1 25 //(2) outerVar = 3 26 //(1) outerVar = 4 27 //(1) outerVar = 1 28 //(2) outerVar = 3 29 //(1) outerVar = 4
这两个内部函数(闭包)引用了同一个局部变量,因此它们共享了一个封闭环境。outerFn()函数实际返回的是一个对象,局部变量outerVar就是这个对象的实例变量,而闭包就是这个对象的实例方法。而且,这些变量也是私有的,因为不能在封装它们的作用域外部直接引用这些变量,从而确保了数据的专有特性。
5.闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个可以读取函数内部的变量。另一个就是让这些变量的值始终保持在内存中。
1 function f1() { 2 var n = 999; 3 nAdd = function() { n += 1} //nAdd全局变量, 也是一个闭包,可以在函数外部对函数内部的局部变量进行操作 4 function f2() { 5 alert(n); 6 } 7 return f2; 8 } 9 10 var result = f1(); 11 result(); //999 12 nAdd(); 13 result(); //1000
在上面代码中,result实际上就是一个闭包。它一共运行了2次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量 n 一直保存在内存中,并没有在f1 调用后被自动清除。
原因在于f1 是f2 的父函数,而f2 被赋给了一个全局变量,导致f2 始终在内存中,而f2 的存在依赖于f1,因此f1 也始终在内存中,不会调用结束后,被垃圾回收机制回收。
6.使用闭包导致的问题
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄漏。解决办法是,在退出函数之前,将不是用的局部变量全部删除。