一道经典的Web前端笔试试题——闭包

永远不要以为自己学了点javascript的DOM操作就以为自己“学会”了JS这门博大精深的语言,其实是自己图样图森破了!!!其实javascript的真正精髓(难点)在于作用域、原型以及闭包这三个方面。这是成为前端高手之前必须要迈过的一道坎

首先我们来看看这样一道题:

1
2
3
4
5
6
7
8
9
10
11
12
var a=0;
function fn(){
for(var i=0;i<10;i++)
{
setTimeout(function(){
a+=i;},0);
}
}
function main(){
console.log(a);
}

这个函数咋一看,没什么了不起的嘛。不就是实现从0加到9的和嘛!很简单,就是45。

显然,世界上大多数的事情总是不能按照我们自己编造的剧本发展滴。实际上,这个main函数的返回结果是100!!!Why?这就涉及到我想要讲的闭包问题。

JS的闭包是比较难理解的一个知识点,我到现在也没有完全理解。实际上,在循环内部的每一个函数中都保留着它的外部函数的活动对象,它们都是指向同一个变量i。当外部函数fn()返回后,变量i的值是10,此时每一个函数都保存着变量i的同一个变量对象,所以在每个函数内部的i的值都是10,所以a+=i实际上是等价于a+=10。循环10次之后的结果显然就变成100了!这其实是因为作用域链的配置机制引发的一个副作用,即闭包只能取得包含函数中任何变量的最后一个值。因为闭包所保存的是整个变量对象,而不是某一个特殊值。
那如果我非要实现从0到9相加怎么办呢?咱们可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a=0;
function fn(){
for(var i=0;i<10;i++)
{
setTimeout(function(num){
return function(){
a+=num;
}
}(i),0);
}
}
function main(){
console.log(a);//45
}

这样,我们没有直接把闭包放进setTimeout函数,而是定义一个匿名函数,并立即执行该匿名函数的结果返回。匿名函数的参数num也就是最终要返回的值。在调用每个匿名函数的时候,我们传入了变量i。由于变量参数是传值的,所以i会复制一个副本给num参数,这样在匿名函数的内部又返回了一个访问num的闭包。所以,每一个内部函数中都有一个自己的num副本,算出来的结果就是0到9相加了,最后结果为45。

我们再来看一道著名互联网公司的实习招聘题目:

这道题的话相对理解起来还是比较容易理解的,如果你搞明白了闭包的含义的话。其实是这样子的:函数a里面有一个匿名函数,这个匿名函数访问了a函数的参数x,形成一个闭包。由于作用域链以及函数声明提前的关系,y函数在代码运行之前就已经预先声明,也就是存在于外部活动变量中,当a函数执行返回的时候,活动变量里面的x值就变成了活动变量里面的X值,即2。