0%

简述Javascript中的闭包

闭包是JavaScript比较有意思的特性,也是比较难搞懂的一个概念。

典型示例

一个比较典型的例子就是打印循环计数——
首先我们写一个小循环,直接打印循环变量i

1
2
3
4
5
function testA() {
for(var i = 0; i < 10; i++) {
console.log("current: " + i);
}
};

这个程序的输出很简单

1
2
3
4
5
6
7
8
9
10
current: 0
current: 1
current: 2
current: 3
current: 4
current: 5
current: 6
current: 7
current: 8
current: 9

接下来做一点小改变——不再循环中立即打印变量了,而是延迟一段时间再打印(类似的是在循环中给div标签添加onClick监听,等到用户点击时再输出变量值),这时候代码变为:

1
2
3
4
5
6
7
function testB() {
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log("current: " + i);
}, 100);
}
}

运行这个方法,输出:

1
2
3
4
5
6
7
8
9
10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10
current: 10

显然没有符合预期,所有延迟的调用都输出了循环变量i最后的值。如果机械的记忆书本上的概念就是闭包只能取得包含函数中任何变量的最后一个值

闭包的使用

换一种思路其实不难理解。

这就好比一个工人生产一批零件,每生产和一个,他就应当在这个零件上打印一个序号。然而这个工人忘记了这道程序,又很不巧打印序号的机器很智能,每监测到生产一个零件,序号就自动加1。当这个工人完成工作以后,他忽然想起忘记打印序号的工序了,他拿起机器就往零件上打印,结果发现所有序号都一样……
这就十分悲剧了。要想打印上正确的序号,必须记住要每生产一个零件以后就打印,万万不可等完工后再来这道工序。

类似的,在setTimeout中,我们传递了一个回调函数延迟调用,回调函数就好比打印序号这道工序,当时没有执行,等到执行时读取的都是变量i,自然拿到的就是最后的值了。
所以就需要循环中每调用一次setTimeout就保存住当前的循环变量i。那这如何实现呢?

下面这段程序可以给我们一些启示:

1
2
3
4
5
6
var num = 5;
function testNum(_num){
_num = 9;
}
testNum(num);
console.log(num); // 5

由于函数参数是按值传递的,传递给testNum的只是num的值,在函数里如何改变型参的值,是不影响原变量的。
这个特性就非常好了,既然我们想保存循环变量的每一个值,那就每循环一步,调用一个函数,把循环变量传进去就好了。这样我们在函数的内部,永远拿到的是调用这个函数时型参对应原变量的值。

1
2
3
4
5
function testB() {
for (var i = 0; i < 10; i++) {
help(i);
}
}

然后在这个方法里再去设置延时任务

1
2
3
4
5
6
7
8
9
10
11
function help(num) {
setTimeout(function () {
console.log("current: " + num);
}, 100);
}

function testB() {
for (var i = 0; i < 10; i++) {
help(i);
}
}

这样程序输出就是1~10了。

进一步优化一下,仅为了保存循环变量,就在外面声明一个函数,非常浪费。在Javascript中更好的做法是声明一个匿名函数,并立即调用它

1
2
3
4
5
6
7
8
9
10
function testB() {
for (var i = 0; i < 10; i++) {
// 匿名函数包含一个参数num
(function(num) {
setTimeout(function () {
console.log("current: " + num);
}, 100);
})(i); // 立即调用了匿名函数,确保i当前值保存在闭包的环境中
}
}

这种写法不太直观,因为我们无法直接看出循环体中做了什么——真正关键的setTimeout是在匿名函数中调用的,多嵌套了一层。如果在循环中直接调用setTimeout意图就更加清晰了。按着这个思路,先将不使用匿名函数的版本做一下调整:

1
2
3
4
5
6
7
8
9
10
11
function help(num) {
return function() {
console.log("current: " + num);
}
}

function testC() {
for (var i = 0; i < 10; i++) {
setTimeout(help(i), 100); // 立即执行help保存i的值
}
}

由于setTimeout第一个参数回调是函数类型,所以我们需要在help方法中返回一个函数,这样调用help后,将返回的匿名函数传递给setTimeout。
同样的,在这里声明一个单独的函数有些浪费,再次改为匿名函数的版本:

1
2
3
4
5
6
7
8
9
function testC() {
for (var i = 0; i < 10; i++) {
setTimeout((function (num) {
return function () {
console.log("current: " + num);
}
})(i), 100);
}
}

setTimeout第一个参数时候,定义了一个返回匿名函数的匿名函数,并立即执行它,达到了同样的效果。

应用案例

最后看一个实际应用的例子。
在node.js中可以使用fs.readdir函数来遍历给定文件夹,返回的结果是文件夹中除.,..以外所有文件/文件夹的数组files

1
2
3
4
var filePath = path.join(__dirname, "someFolderName");
fs.readdir(filePath, function (err, files) {
parseResult(err, files);
});

继而在parseResult中,我们想对这个数组进行处理,筛选出其中的文件夹进行进一步的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function parseResult(err, files) {
var length = 0;
for (var tmp in files) {
length++;
}

for (var item in files) {
var filePath = path.join(__dirname, "someFoladerName/" + files[item]);

fs.stat(filePath, (function (num) {
console.log("the num is " + num);
var currentItem = num;
return function (err, stats) {
if (stats.isDirectory) {
// 检测到文件夹,做相应处理
}

if (currentItem == (length - 1)) { // currentItem是string类型的序号,转换后判断
// 对所有文件/文件夹进行的stat异步调用完成后,进行最后操作
}
}
})(item)); // 立即调用匿名函数,保存item当前值并返回其中声明的处理函数

}
}