官方:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。
通俗:闭包就是能够读取其他函数内部变量的函数。
要理解闭包,首先必须理解Javascript特殊的变量作用域。
变量的作用域无非就是两种:全局变量和局部变量。
Javascript语言的特殊之处,就在于函数内部可以直接读取全局变量。
另一方面,在函数外部自然无法读取函数内的局部变量。
这里有一个地方需要注意,函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明了一个全局变量!
我们有时候需要得到函数内的局部变量。但是,正常情况下这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数。
function f1(){
n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1 就是不可见的。这是由于Javascript语言特有的“链式作用域”结构(chain scope)决定的,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
function f1(){
n=999;
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
function foo(){
var local = 1
function bar(){
local++
return local
}
return bar
}
var func = foo()
func()
这里面确实有闭包,local 变量和 bar 函数就组成了一个闭包(Closure)。
为什么要函数套函数呢?
是因为需要局部变量,所以才把 local 放在一个函数里,如果不把 local 放在一个函数里,local 就是一个全局变量了,达不到使用闭包的目的——隐藏变量。
有些人看到“闭包”这个名字,就一定觉得要用什么包起来才行。其实这是翻译问题,闭包的原文是 Closure,跟包没有任何关系。
var name = "The Window";
var object = {
name : "My Object",
getNameFunc : function(){
return function(){
return this.name;
};
}
};
alert(object.getNameFunc()()); //"The Window"
我们知道,this对象是在运行时基于函数的执行环境绑定的,在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性。每个函数在被调用时,其活动对象都会自动取得两个特殊变量:this和arguments.内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。
不过可以通过把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问到该对象了。
如下所示:
var name = "The Window";
var object = {
name: "My Object",
getNameFunc: function() {
var that = this;
return function() {
return that.name;
};
}
};
alert(object.getNameFunc()()); //"My Object"
这里在定义匿名函数之前,我们把this对象赋值给了一个名叫that的变量,而在定义了闭包之后,闭包也可以访问这个变量,因为它是我们在包含函数中特意声明的一个变量。即使在函数返回之后,that也仍然引用着object,所以调用object.getNameFunc()()就返回了"My Object"。(this和arguments也存在同样的问题,如果想访问作用域中的arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。)
闭包可以用在许多地方。它的最大用处有三个,
怎么来理解呢?下面给出两个例子。
function f1(){
var n=999;
AD*D=function(){n+=1}
function f2(){
alert(n);
}
return f2;
}
var result=f1();
result(); // 999
ADD();
result(); // 1000
在这段代码中,result实际上就是闭包f2函数。
result一共运行了两次,第一次的值是999,第二次的值是1000。这证明了,函数f1中的局部变量n一直保存在内存中,并没有在f1调用后被自动清除。
为什么会这样呢?
原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
注意:“ADD=function(){n+=1}”这一行,首先在ADD前面没有使用var关键字,因此 ADD是一个全局变量,而不是局部变量。其次,ADD的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以ADD相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
假设我们在写交易项目中,关于“还剩多少钱”的代码。
如果不用闭包,你可以直接用一个全局变量:
window.money = 500 // 还有五百块
这样看起来很不安全,万一不小心把这个值改了怎么办。所以我们不能让别人“直接访问”这个变量,所以要用到局部变量。
但是用局部变量别人又访问不到,怎么办呢?
暴露一个访问器(函数),让别人可以“间接访问”。
代码如下:
!function(){
var money = 500
window.addmoney = function(){
money += 10
}
window.submoney = function(){
money -= 10
}
}()
那么在其他的 JS 文件,就可以使用 window.addmoney() 来增加钱包,使用 window.submoney() 来扣除余额。
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题;
在IE中可能导致内存泄露。IE 有 bug,在我们使用完闭包之后,IE依然回收不了闭包里面引用的变量。
解决方法:在退出函数之前,将不使用的局部变量全部删除。
闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(public method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
例1
function sayHello(name)
{
var text = 'Hello ' + name;
var sayAlert = function() { console.log(text); }
sayAlert();
}
sayHello("Bob") // 输出"Hello Bob"
在sayHello()函数中定义并调用了sayAlert()函数;sayAlert()作为内层函数,可以访问外层函数sayHello()中的text变量。
例2
function sayHello2(name)
{
var text = 'Hello ' + name; // 局部变量
var sayAlert = function() { console.log(text); }
return sayAlert;
}
var say2 = sayHello2("Jane");
say2(); // 输出"Hello Jane"
例3
function buildList(list) {
var result = [];
for(var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result.push(
function() {
console.log(item + ' ' + list[i]);
}
);
}
return result;
}
var fnlist = buildList([1,2,3]);
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
得到的结果:连续输出3个"item3 undefined"
解析:通过执行buildList函数,返回了一个result,那么这个result存放的是3个匿名函数。然而这三个匿名函数其实就是三个闭包,因为它可以访问到父函数的局部变量。所以闭包内的保留的i是最终的值为3.所以list[3]肯定是undefined. item变量值为item3.
改成如下代码:
function buildList(list) {
var result = [];
for(var i = 0; i < list.length; i++) {
var item = 'item' + list[i];
result.push(
(function(i) {
console.log(item + ' ' + list[i]);
})(i)
);
}
return result;
}
var fnlist = buildList([1,2,3]);
得到的结果:
item1 1
item2 2
item3 3
解释:这儿虽然传递了一个数组进去,但是返回的是三个自执行的函数。
例4
function newClosure(someNum, someRef)
{
var anArray = [1,2,3];
var num = someNum;
var ref = someRef;
return function(x)
{
num += x;
anArray.push(num);
console.log('num: ' + num + "; " + 'anArray ' + anArray.toString() + "; " + 'ref.someVar ' + ref.someVar);
}
}
closure1 = newClosure(40, {someVar: "closure 1"});
closure2 = newClosure(1000, {someVar: "closure 2"});
closure1(5); // 打印"num: 45; anArray 1,2,3,45; ref.someVar closure 1"
closure2(-10); // 打印"num: 990; anArray 1,2,3,990; ref.someVar closure 2"
每次调用newClosure()都会创建独立的闭包,它们的局部变量num与ref的值并不相同。
例5
function sayAlice()
{
var sayAlert = function() { console.log(alice); }
var alice = 'Hello Alice';
return sayAlert;
}
var sayAlice2 = sayAlice();
sayAlice2(); // 输出"Hello Alice"
alice变量在sayAlert函数之后定义,这并未影响代码执行。因为返回函数sayAlice2所指向的闭包会包含sayAlice()函数中的所有局部变量,这自然包括了alice变量,因此可以正常打印”Hello Alice”。
例6
function setupSomeGlobals() {
var num = 666;
gAlertNumber = function() { console.log(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals();
gAlertNumber(); // 输出666
gIncreaseNumber();
gAlertNumber(); // 输出667
gSetNumber(5);
gAlertNumber(); // 输出5
解释:首先gAlertNumber,gIncreaseNumber,gSetNumber是三个全局变量,并且其三个值都是匿名函数,然而这三个匿名函数本身都是闭包。他们操作的num都是保存在内存中的同一个num,所有会得出上面的结果。
下面看一个dom操作,使用闭包的例子:
// 这个代码是错误的,因为变量i从来就没背locked住
// 相反,当循环执行以后,我们在点击的时候i才获得数值
// 因为这个时候i操真正获得值
// 所以说无论点击那个连接,最终显示的都是I am link #10(如果有10个a元素的话)
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am link #' + i);
}, 'false');
}
最终结果都显示为I am link #10(如果有10个a元素的话),这是作用链域的分配机制导致的。从表面上看,似乎每次点击某个连接的时候都应该返回自己的索引值。即点击位置为0的链接应该返回0,点击位置为1的链接应该返回1,以此类推。但实际上,点击每个链接都返回链接数的最大值,这是因为每个elems[i].addEventListener
他们都保存着全局的活动对象,所以他们引用的都是同一个变量i(按引用传递)。
解决方法:通过创建另一个匿名函数,强制使结果符合预期。
// 这个是可以用的,因为他在自执行函数表达式闭包内部
// i的值作为locked的索引存在,在循环执行结束以后,尽管最后i的值变成了a元素总数(例如10)
// 但闭包内部的lockedInIndex值是没有改变,因为他已经执行完毕了
// 所以当点击连接的时候,结果是正确的
var elems = document.getElementsByTagName('a');
for (var i = 0; i < elems.length; i++) {
(function (lockedInIndex) {
elems[i].addEventListener('click', function (e) {
e.preventDefault();
alert('I am link #' + lockedInIndex);
}, 'false');
})(i);
}
这里匿名函数有一个参数lockedInIndex,也就是点击连接时最终要返回的结果值,在调用匿名函数时,我们传入了变量i,由于函数的变量是按值传递的,所以会将当前变量的值复制给lockedInIndex,所以最终我们能够得到预期的结果。
理解Javascript的闭包是迈向高级JS程序员的必经之路,理解了其解释和运行机制才能写出更为安全和优雅的代码。