Javascript闭包的一些研究
本文不谈闭包的概念,因为概念容易把人搞晕,本文希望通过几个鲜活的例子来探究闭包的性质,相信对理解闭包会有所帮助。
程序1
var f = (function() { var n = 10; return function() { ++n; console.log(n); } })(); f();
输出:
11
结论:
闭包函数可以访问外层函数中的变量。
程序2
var arr = []; (function() { var n = 0; for (var i=0; i<3; ++i) { arr[i] = function() { ++n; console.log(n); } } })(); for (var i=0; i<3; ++i) { var f = arr[i]; f(); }
输出:
1 2 3
结论:
一个函数内有多个闭包函数,那么这些闭包函数共享外层函数中的变量。可以看出,例子中3个闭包函数中的n是同一个变量,而不是该变量的3个副本。
程序3
var f0 = function() { ++n; console.log(n); } var f = (function() { var n = 10; return f0; })(); f();
输出:
错误指向“++n”这一行。
结论:
闭包函数的作用域不是在引用或运行它的时候确定的,看起来好像是在定义它的时候确定的。
说明:
虽然该程序与“程序1”看起来一样,但由于函数f0一个在内部定义一个在外部定义,尽管它们都是从函数内部返回,但在这个例子中f0却无法访问变量n。
程序4
var f = (function() { var n = 10; return new Function('++n;console.log(n);'); })(); f();
输出同“程序3”:
结论:
该程序是对“程序3”的进一步印证和补充。由该程序可以得出的结论是:闭包函数的作用域是在编译它的时候确定的。
说明:
使用Function构造器可以创建一个function对象,函数体使用一个字符串指定,它可以像普通函数一样执行,并在首次执行时编译。因此,虽然在匿名函数内部定义了此function对象,但一直要到调用它的时候才会编译,即执行到“f()”时,而此时原函数的作用域已经不存在了。
再看一个例子:
程序5
var f = (function() { var n = 10; return eval('(function(){++n;console.log(n);})'); })(); f();
输出:
11
结论:无
说明:
这个例子是对上面两个程序的补充。这个例子之所以能够和“程序1”一样打印出11,是因为eval( )函数会立即对传递给它字符串进行解析(编译、执行),因此使用eval定义的函数和直接定义的效果是等价的。
(注意:eval( )中的“function(){...}”必须用括号扩起来,否则会报错)
程序6
var f = (function() { var n = 10; var s = 'hello'; return function() { ++n; console.log(n); } })(); f();
运行时在“console.log(n);”这一行加个断点,查看作用域中的值,其中只有n没有s:
结论:
外层函数中那些未在闭包函数中使用的变量,对闭包函数是不可见的。在这个例子中,闭包函数没有引用过变量s,因此其作用域中只有n。也就是说,对闭包函数来说,其可以访问的外层函数的变量实际上只是真正的外层函数变量的一个子集。
程序7
这个程序用来通过数据证明“程序6”的结论。程序稍微有点复杂,后面会先对其做简单说明。
var funArr = []; var LENGTH = 500; var ALPHABET = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; function getStr( ) { var s = ''; for (var i=0; i<LENGTH; ++i) { s += ALPHABET[Math.floor(Math.random( ) * 62)]; } return s; } function getArr( ) { var a = new Array(LENGTH); for (var i=0; i<LENGTH; ++i) { a[i] = Math.random( ); } } var f = function( ) { var n = 10; var s = getStr( ); var a = getArr( ); funArr.push(function( ) { console.log(n, s, a); // --- 1 console.log(n); // --- 2 }) } for (var i=0; i<2000; ++i) { f( ); }
程序分析:
本程序的重点是for循环和函数f。for循环中调用了函数f 2000次,每次调用都会创建一个数字和两个长度为500的字符串和数组,所以2000次函数调用所创建的局部变量的规模还是比较可观的,程序用这种方法以便于后面做分析时对结果进行比较。
f中的局部变量会被一个闭包函数所引用,以此观察未被引用的局部变量是否会被回收。
分别运行该程序两次,第一次使用语句1(引用了f中的所有局部变量),第二次使用语句2(只引用了数字变量n)。对运行所得到的结果1和结果2分别采集堆快照(Heap Snapshot):
可以看到所占内存差别巨大,从这里就可以初步得出“未被闭包函数引用的局部变量会被回收”的结论。
不过为了严谨性,需要做更细致地分析。首先是结果1和结果2的统计图:
可以看到,第二次运行后内存中的对象数量明显要比第一次的少很多(二者产生的中间对象数量是相同的),这进一步说明了第二次运行后大部分对象都被回收了。
最后我们将结果2与结果1进行细致的比较,结果如下:
结论:
函数中的局部变量如果没有被任何闭包函数所引用(这里不考虑被全局变量所引用的情况),则这些局部变量在函数运行完成后就可以被回收,否则,这些变量会作为其闭包函数的作用域的一部分被保留,直到所有闭包函数也执行完毕为止。
该结论同时也印证了“程序6”的结论:闭包函数对外层函数作用域的引用是外层函数真实作用域的一个子集。
另外从这个实验还能推测出一点,即外层函数执行结束后是会被回收掉的。因为既然函数内部变量已被回收,那函数本身也没有存在的意义了。