问题

Javascript 到处充满着神(sha)奇(bi)的地方。
面试中经常被问到的一个题目:

1
2
3
4
5
6
for (a = 0; a < 5; a++) {
setTimeout(function() {
console.log(a);
}, 0);
}
// 5 5 5 5 5

此处,变量 a 未声明即使用,在 js 中不会出现语法错误。详细的解释可以参考 你听说过「变量提升」吗?

js 中的丰富的异步编程,使其简单优雅而高效。但也不免带来了理解上的成本。这个示例中,setTimeout 便是一个常见的异步延迟方法,即使延迟时间是 0。

然我们从头来看一遍这个代码,主脚本程序执行 for 循环,由于 javascript解释器event loop 机制。setTimeout 作为 消息 被添加到消息队列,待当前任务(即主脚本程序)执行完成后,才会处理消息队列中的其他任务。详细内容可以查看 并发模型与事件循环

所以,等到 setTimeout 要执行时,当前函数帧的变量 a 已经被赋值为 5, 故会输出 5。

如果我们想要实现输出 0 1 2 3 4,有下面几种方法:

  1. 使用 ES6 中的 let
1
2
3
4
5
6
for (let a = 0; a < 5; a++) {
setTimeout(function() {
console.log(a);
}, 0);
}
// 0 1 2 3 4
  1. 使用闭包

这么半天终于遇到闭包了。闭包是有权访问另一个函数作用域中变量的函数。,修改代码如下:

1
2
3
4
5
6
7
8
for (a = 0; a < 5; a++) {
(function(b) {
setTimeout(function() {
console.log(b);
}, 0);
})(a);
}
// 0 1 2 3 4

这里,先构建了一个匿名函数,将 a 作为参数传入。setTimeout 的回调函数就形成了闭包,其可以访问匿名函数作用域中的 b。每次执行 for 循环,即形成一个匿名函数作用域,而这个作用域中的变量 b 则是根据传入的形参 a 所确定。所以就正常的返回了 0 1 2 3 4

闭包

要理解闭包,首先我们要了解 js 的作用域链。js 中,当一个函数被调用,会创建一个执行环境及相应的作用域链,然后使用 arguments 和其他命名参数值的来初始化函数的活动对象。正常情况下,我们在函数中访问一个变量时,就会从该函数的作用域链中搜索具有相应名字的变量,函数执行完毕后,局部变量会被销毁,内存中仅保留全局作用域。但是,闭包的情况又有所不同。

闭包的作用域链不仅包含函数作用域和全局作用域,还包括所能访问的父函数的函数作用域。这时候,父函数执行完毕,并不会销毁其函数作用域,因为还被闭包所引用。除非等到闭包被销毁后,父函数作用域也会被销毁。

这其实也不难理解,js 的函数式编程运行函数执行后,返回另一个函数。这在很多框架中被广泛应用。

1
2
3
4
5
6
7
8
9
10
function a() {
var value = 'aaa';
return function() {
// apply(this, value);
console.log(value);
};
}

var b = a();
b = null;

如上代码,函数 a 返回一个匿名函数,当 a() 执行后,其实 a 是将其作用域传递给匿名函数。匿名函数当然也就可以返回 value。待 b = null; 被销毁后,a 也就被销毁。

闭包的优缺点

保护函数内的变量安全,加强了封装性。但是闭包会增加内存的占用,有内存泄漏的风险。