这几天看到闭包一章,从工具书到各路大神博客,都各自有着不同的理解,以下我将选择性的抄(咳咳,当然还是会附上自己理解的)一些大神们对闭包的原理及其使用文章,当作是自己初步理解这一功能函数的过程吧。
首先先上链接:
简书作者波同学的JS进阶文章系列:
前端基础进阶系列
其他:
JS秘密花园
javascript深入理解js闭包
阮一峰《JavaScript标准参考教程》
一不小心就做错的JS闭包面试题
还有一些也很不错,但主要是以应用为主,原理解释没有上面几篇深入,不过作为闭包的拓展应用其实也可以看一看;
红皮书《JS高程》的闭包:
闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数。
从这句话我们知道:闭包是一个函数
function createComparisonFunction(propertyName) {
return function(obj1,obj2) {
var value1 = obj1[propertyName];
var value2 = obj2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
这段代码,我们能直接看出,共存在三个作用域,Global、createComparisonFunction、匿名函数funciton,因其JS的作用域链特性,后者能访问自身及前者的作用域。而返回的匿名函数即使在其他地方被调用了,但它仍可以访问变量propertyName。之所以还能够访问这个变量,是因为内部函数的作用域链中包含createComparisonFunction的作用域。我们来深入了解一下,函数执行时具体发生了什么?
当第一个函数被调用时,会创建一个执行环境(Execution Context,也叫执行上下文)及相应的作用域链,并把作用域链赋值给一个特殊的内部属性[[Scope]]。然后,使用this、arguments和其他命名参数的值来初始化函数的活动对象(Activation Object)。但在作用域链中,外部函数的活动对象处于第二位,外部函数的外部函数处于第三位,最后是全局执行环境(Global Context)。
换一个栗子:
function createFunctions() {
var result = new Array();
for (var i=0;i<10;i++) {
result[i] = function() {
return i;
};
}
return result;
}
var arr = createFunctions();
alert(arr[0]()); // 10
alert(arr[1]()); // 10
/这个函数返回一个函数数组。表面上看,似乎每个函数都应该返回自己的索引值,位置0的函数返回0,位置1的函数返回1,以此类推。但但实际上,每个函数都返回10,为什么?
数组对象内的匿名函数里的i是引用createFunctions作用域内的,当调用数组内函数的时候,createFunctions函数早已执行完毕。
这图不传也罢了,画得忒丑了。
数组内的闭包函数指向的i,存放在createFunctions函数的作用域内,确切的说,是在函数的变量对象里,for循环每次更新的i值,就是从它那儿来的。所以当调用数组函数时,循环已经完成,i也为循环后的值,都为10;
有人会问,那result[i]为什么没有变为10呢?
要知道,作用域的判定是看是否在函数内的,result[i] = function.......
是在匿名函数外,那它就还是属于createFunctions
的作用域内,那result[i]
里的i就依然会更新
那么如何使结果变为我们想要的呢?也是通过闭包。
function createFunctions() {
var result = [];
for (var i=0;i<10;i++) {
!function(i) {
result[i] = function() {console.log(i)};
}(i);
}
return result;
}
var arr = createFunctions();
arr[0]();
arr[1]();
arr[2]();
function createFunctions() {
var result = [];
function fn(i) {
result[i] = function() {console.log(i)}
};
for (var i=0;i<10;i++) {
fn(i);
}
return result;
}
var arr = createFunctions();
arr[0]();
arr[1]();
arr[2]();
var arr = [];
function fn(i) {
arr[i] = function() {console.log(i)}
}
function createFunctions() {
for (var i=0;i<10;i++) {
fn(i);
}
}
fn(createFunctions());
arr[0]();
arr[1]();
arr[2]();
以第一种为例,通过一个立即调用函数,将外函数当前循环的i
值作为实参传入,并存放在立即调用函数的变量对象内,此时,这个函数立即调用函数和数组内的匿名函数就相当于一个闭包,数组的匿名函数引用了立即调用函数变量对象内的i。当createFuncions
执行完毕,里面的i值已经是10了。但是由于闭包的特性,每个函数都有各自的i值对应着。对数组函数而言,相当于产生了10个闭包。
所以能看出,闭包也十分的占用内存,只要闭包不执行,那么变量对象就无法被回收,所以不是特别需要,尽量不使用闭包。
关于this
对象
在闭包中使用this对象也会导致一些问题。我们知道,this对象是在运行时基于函数的执行环境绑定的;在全局对象中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。但有时候由于编写闭包的方式不同,这一点可能不会那么明显。(当然可以用call和apply)
var name = "The Window";
var obj = {
name:"My Object",
getName:function () {
var bibao = function () {
return this.name;
};
return bibao;
}
};
alert(obj.getName()()); // The Window
先创建一个全局变量name,又创建一个包含name属性的对象。这个对象包含一个方法——getName(),它返回一个匿名函数,而匿名函数又返回this.name。由于getName()返回一个函数,因此调用obj.getName()();就会立即调用它返回的函数,结果就是返回一个字符串。然而,这个例子返回的字符串是"The Window",即全局name变量的值。为什么匿名函数没有取得其波包含作用域(或外部作用域)的this对象呢?
每个函数调用时其活动对象都会自动取得两个特殊变量:this
和arguments
。
内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。
var name = "The Window";
var obj = {
name:"My Object",
getName:function () {
var that = this;
return function () {
return that.name;
};
}
};
alert(obj.getName()());
this
和arguments
也存在同样的问题,如果想访问作用域中arguments对象,必须将该对象的引用保存到另一个闭包能够访问的变量中。var name = "The Window"; var obj = { name:"My Object", getName:function (arg1,arg2) { var arg = []; arg[0] = arg1; arg[1] = arg2; function bibao() { return arg[0]+arg[1]; } return bibao; } }; alert(obj.getName(1,2)())
obj.getName方法保存了其接收到的实参在它的变量对象上,并在执行函数结束后没有被回收,因为返回的闭包函数引用着obj.Name方法里的arg数组对象。使得外部变量成功访问到了函数内部作用域及其局部变量。
在几种特殊情况下,this引用的值可能会意外的改变。
var name = "The Window";
var obj = {
name:"My Object",
getName:function () {
return this.name;
}
};
这里的getName()只简单的返回this.name的值。
var name = "The Window";
var obj = {
name:"My Object",
getName:function () {
console.log(this.name);
}
};
obj.getName(); // "My Object"
(obj.getName)(); // "My Object"
(obj.getName = obj.getName)(); // "The Window"
第一个obj.getName
函数作为obj
对象的方法调用,则自然其this
引用指向obj
对象。
第二个,加括号将函数定义之后,作为函数表达式执行调用,this
引用指向不变。
第三个,括号内先执行了一条赋值语句,然后在调用赋值后的结果。相当于重新定义了函数,this
引用的值不能维持,于是返回"The Window"。
闭包与setTimeout()
用setTimeout
结合循环考察闭包是一个很老的面试题了
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
setTimeout
的执行与我们平常的JS代码执行不一样,这里需要提到一个队列数据结构执行的概念。
关于setTimeout与循环闭包的思考题
个人理解:由于setTimeout
函数的特殊性,须等其他非队列结构代码执行完毕后,这个setTimeout
函数才会进入队列执行栈。
用chrome开发者工具分析这段代码,可以先自己分析一次,看看依次弹出什么?
setTimeout(function() {
console.log(a);
}, 0);
var a = 10;
console.log(b);
console.log(fn);
var b = 20;
function fn() {
setTimeout(function() {
console.log('setTImeout 10ms.');
}, 10);
}
fn.toString = function() {
return 30;
}
console.log(fn);
setTimeout(function() {
console.log('setTimeout 20ms.');
}, 20);
fn();
答案:
设置断点如图所示,今天刚学Chrome的开发者工具,有哪些使用上的错误还请指出。
我分别给变量a、b、fn函数都设置了观察,变量的值变化将会实时地在右上角中显示,可以看到,在JS解释器运行第一行代码前,变量a、b就已经存在了,而fn函数已经完成了声明。接下来我们继续执行。要注意:蓝色部分说明这些代码将在下一次操作中执行,而不是已经执行完毕。
把第一个setTimeout函数执行完毕后也没有反应。我给三个setTimeout内的匿名函数也加上观察选项,却显示不可使用。
所以,下一次执行会发生什么?对console出b的值,但是b没赋值,右上角也看到了,所以显示undefined。
而console.log(fn)就是将fn函数函数体从控制台弹出,要注意,console会隐式调用toString方法,这个会在后面讲到。
现在第26行之前(不包括26行)的代码都已略过,a,b变量也已得到赋值,继续执行。
重写了toString
方法前:
重写后:
toString方法是Object所有,所有由它构造的实例都能调用,现在这个方法被改写并作为fn对象的属性(方法)保留下来。
console会隐式调用toString方法,所以30行的console会弹出30;
继续执行,定义setTimeout函数也是什么没有发生,知道调用fn前。
调用fn,是不是就会执行setTimeout函数呢?其实没有,我们可以看到call stack一栏已经是fn的执行栈了,但是依旧没发生什么。
但是:
当call stack里的环境都已退出,执行栈里没有任何上下文时,三个setTimeout函数就执行了,那这三个时间戳函数那个先执行,那个后执行呢?由设定的延迟时间决定,这个延迟时间是相对于其他代码执行完毕的那一刻。
不信我们可以通过改变延迟时间重新试一次就知道了。
我们在看回原来的闭包代码:
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}
先确认一个问题,setTimeout
函数里的匿名函数的i指向哪儿?对,是全局变量里的i。setTimeout
里的匿名函数执行前,外部循环已经结束,i值已经更新为6,这时setTimeout
调用匿名函数,里面的i当然都是6了。
我们需要创建一个能够保存当前i值的"盒子"给匿名函数,使得匿名函数能够引用新创建的父函数。
// 利用闭包,修改下面的代码,让循环输出的结果依次为1, 2, 3, 4, 5
for (var i=1; i<=5; i++) {
!function (i) {
setTimeout( function timer() {
console.log(i);
}, i*1000 );
}(i);
}
自调用函数就是那个"盒子"
关于《JavaScript编程全解》中的说明
考虑这个函数:
function f(arg) {
var n = 123 + Number(arg);
function g() {console.log("n is "+n);console.log("g is called");}
n++;
function gg() {console.log("n is "+n);console.log("g is called");}
return [g,gg];
}
调用数组内函数的console结果是什么?
var arr = f(1);
arr[0](); // 对闭包g的调用
// "n is 125" "g is called"
arr[1](); // 对闭包gg的调用
// "n is 125" "gg is called"
函数g与函数gg保持了各自含有局部变量n的执行环境。由于声明函数g时与声明函数gg时的n值是不同的,因此闭包g与闭包gg貌似将会表示各自不同的n值。实际上两者都将表示相同的值。因为它们引用了同一个对象。
即都是引用了,f
函数执行环境内变量对象内的n
值。当执行f(1)
的时候,n值就已经更新为最后计算的值。
防范命名空间的污染
模块:
在JavaScript中,最外层代码(函数之外)所写的名称(变量名与函数名)具有全局作用域,即所谓的全局变量与全局函数。JavaScript的程序代码即使在分割为多个源文件后,也能相互访问其全局名称。在JavaScript的规范中不存在所谓的模块的语言功能。
因此,对于客户端JavaScript,如果在一个HTML文件中对多个JavaScript文件进行读取,则他们相互的全局名称会发生冲突。也就是说,在某个文件中使用的名称无法同时在另一个文件中使用。
即使在独立开发中这也很不方便,在使用他们开发的库之类时就更加麻烦了。
此外,全局变量还降低了代码的可维护性。不过也不能就简单下定论说问题只是由全局变量造成的。这就如同在Java这种语言规范并不支持全局变量的语言中,同样可以很容易创建出和全局变量功能类似的变量。
也就是说,不应该只是一昧地减少全局变量的使用,而应该形成一种尽可能避免使用较广的作用域的意识。对于较广的作用域,其问题在于修改了某处代码之后,会难以确定该修改的影响范围,因此代码的可维护性会变差。
避免使用全局变量
从形式上看,在JavaScript中减少全局变量的数量的方法时很简单的。首先我们按照下面的代码这样预设一下全局函数与全局变量。
// 全局函数
function sum(a,b) {
return Number(a)+Number(b);
}
// 全局变量
var position = {x:2,y:3};
// 借助通过对象字面量生成对象的属性,将名称封入对象的内部。于是从形式上看,全局变量减少了
var MyModule = {
sum:function (a,b) {
return Number(a)+Number(b);
},
position:{x:2,y:3}
};
alert(MyModule.sum(3,3)); // 6
alert(MyModule.position.x); // 2
上面的例子使用对象字面量,不过也可以像下面这样不使用对象字面量。
var MyModule = {}; // 也可以通过new表达式生成
MyModule.sum = function (a,b) {return Number(a)+Number(b);};
MyModule.position = {x:2,y:3};
这个例子中,我们将MyModule称为模块名。如果完全采用这种方式,对于1个文件来说,只需要一个模块名就能消减全局变量的数量。当然,模块名之间仍然可能产生冲突,不过这一问题在其他程序设计语言中也是一个无法被避免的问题。
通过这种将名称封入对象之中的方法,可以避免名称冲突的问题。但是这并没有解决全局名称的另一个问题,也就是作用域过广的问题。通过MyModule.position.x这样一个较长的名称,就可以从代码的任意一处访问该变量。
通过闭包实现信息隐藏
// 在此调用匿名函数
// 由于匿名函数的返回值是一个函数,所以变量sum是一个函数
var sum = (function () {
// 无法从函数外部访问该名称
// 实际上,这变成了一个私有变量
// 一般来说,在函数被调用之后该名称就无法再被访问
// 不过由于是在被返回的匿名函数中,所以仍可以继续被使用
var p = {x:2,y:3};
// 同样是一个从函数外无法被访问的私有变量
// 将其命名为sum也可以。不过为了避免混淆,这里采用其他名称
function sum_internal(a,b) {
return Number(a)+Number(b);
}
// 只不过是为了使用上面的两个名称而随意设计的返回值
return function (a,b) {
alert("x = "+p.x);
return sum_internal(a,b);
}
})();
console.log(sum(3,4));
// "x = 2"
// "y"
上面的代码可以抽象为下面这种形式的代码。在利用函数作用域封装名称,以及闭包可以使名称在函数调用结束后依然存在这两个特性。这样信息隐藏得以实现。
(function(){函数体})();
像上面这样,当场调用函数的代码看起来或许有些奇怪。一般的做法是先在某处声明函数,之后在需要时调用。不过这种做法是JavaScript的一种习惯用法,加以掌握。
匿名函数的返回值是一个函数,不过即使返回值不是函数,也同样能采用这一方法。比如返回一个对象字面量以实现信息隐藏的功能。
var obj = (function() {
// 从函数外部无法访问该名称
// 实际上,这是一个私有变量
var p = {x:2,y:3};
// 这同样是一个无法从函数外部访问的私有函数
function sum_internal(a,b) {
return Number(a+b);
}
// 只不过为了使用上面的两个名称而随意设计的返回值
return {
sum:function (a,b) {
return sum_internal(a,b);
},
x:p.x
};
})();
alert(obj.sum(3,4)); // 7
alert(obj.x); // 2
闭包与类
利用函数作用域与闭包,可以实现访问在控制,上一节中,模块的函数在被声明之后立即就对其调用,而是用了闭包的类则能够在生成实例时调用。即便如此,着厚重那个做法在形式上仍然只是单纯的函数生命。下面是一个通过闭包来对类进行定义的例子
// 用于生成实例的函数
function myclass(x,y) {
return {show:function () {alert(x+" | "+y)}};
}
var obj = myclass(3,2);
obj.show(); // 3 | 2
这里再举一个具体的例子,一个实现了计数器功能的类。
这里重申一下:JavaScript的语言特性没有"类"的概念。但这里的类指的是,实际上将会调用构造函数的Function对象。此外在强调对象是通过调用构造函数生成的时候,会将这些被生成的对象称作对象实例以示区别。
表达式闭包
JavaScript有一种自带的增强功能,称为支持函数型程序设计的表达式闭包(Expression closure
)。
从语法结构上看,表达式闭包是函数声明表达式的一种省略形式。可以像下面这样省略只有return
的函数声明表达式中的return
与{}
。
var sum = function (a,b) {return Number(a+b)};
// 可以省略为
var sum = function (a,b) Number(a+b);
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。