词法作用域之经典面试题

不知道各位是否遇到过这样的面试题?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title></title>
<script type="text/javascript">
function load(){
var arr = document.getElementsByTagName("p");
for(var i = 0; i < arr.length; i++){
arr[i].onclick = function(){
alert(i);
}
}
}
</script>
</head>
<body onload="load()">
<p>0</p>
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
</body>
上面这段代码像实现点击p标签,弹出该p标签位置序号?请问上述代码能否实现该需求,如果不能,应该如何实现?

咋一看感觉没问题,其实这里面蕴藏玄机
我们来看一下执行结果,随意点击一个2
LexicalScope
为什么是5,为什么不是2?unbelievable
实际上点击0-4之间随意一个都是5

为什么?

首先解释 5 是从哪里来的。这个循环的终止条件是 i 不再 < 5 。条件首次成立时 i 的值是5。因此,输出显示的是循环结束时 i 的最终值。
实际上所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 5 出来。
这里引伸出一个更深入的问题,代码中到底有什么缺陷导致它的行为同语义所暗示的不一致呢?
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i 。这样说的话,当然所有函数共享一个 i 的引用。循环结构让我们误以为背后还有更复杂的
机制在起作用,但实际上没有。如果将函数的回调重复定义五次,完全不使用循环,那它同这段代码是完全等价的。下面回到正题。缺陷是什么?我们需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。

那我们要如何改造才能行为同语义?

我们把循环改为如下:

1
2
3
4
5
6
7
for(var i = 0; i < arr.length; i++){
(function(j) {
arr[j].onclick = function(){
alert(j);
}
})( i );
}

LexicalScope2

原理是

在迭代内使用 立即执行函数会为每个迭代都生成一个新的作用域,使得函数的回调可以将新的作用域封闭在每个迭代内部,每个迭代中都会含有一个具有正确值的变量供我们访问。

我们还有更简单方案如下:

1
2
3
4
5
6
for(let i = 0; i < arr.length; i++){
arr[i].onclick = function(){
alert(i);
}
};
}

LexicalScope3
只要改变一个var 为let 能得到这样意想不到的效果

let声明,可以用来劫持块作用域,并且在这个块作用域中声明一个变量。本质上这是将一个块转换成一个可以被关闭的作用域

结尾

举一反三,如果遇到底下的代码,我们是不是可以一眼看出输出啥?

1
2
3
4
5
for(var i = 0 ; i < 10 ; i++){
setTimeout(function(){
console.log(i);
 },1000);
}