JavaScript面试题
题目
话不多说,直接上题! 下面代码的输出结果是什么?
var value1 = 0;
var value2 = 0;
var value3 = 0;
for( var i = 10; i > 0; i-- ){
var i2 = i;
(function(){
var i3 = i;
setTimeout(function(){
value1 += i;
value2 += i2;
value3 += i3;
}, 1);
})();
}
setTimeout(function(){
console.log(value1, value2, value3);
}, 100);
思考一下你的答案... 10 9 8 7 6 5 4 3 2 1
答案:
0 10 55
可能除了部分老前端,大部分人第一次见到这个题目都会答错。因为这行为太诡异了,不符合我们大部分编程语言的逻辑思维。尤其是咋们常年做后端的朋友们(包括我,一个年轻的后端),看到这输出,什么鬼??哈哈,不要惊慌,这可以说是由JavaScript的缺陷引起的,没错,缺陷!下面我们来分析一下为什么会出现这个诡异的输出。
解析
首先我们需要说一说关于var的两个特性:变量提升和没有块作用域。
什么是变量提升
其实很简单,脚本运行的时候,变量的声明会统一提升到作用域的头部。也就是说,脚本一运行所有的全局变量就已经声明了(但还有没有赋值)。举个例子
var v1 = 111;
console.log(v2) // 不会报错,输出undefined
// do some thing ....
// a lot of code
var v2 = 222;
上面的代码运行时其实是下面的样子:
var v1;
var v2;
v1 = 111;
console.log(v2); //没有报错而输出undefined,已经很明了了
// do some thing ....
// a lot of code
v2 = 222;
块级作用域
对于var来说,是没有块级作用域的(有方法作用域啊)。也就是说,你可以在块外使用块内声明的变量。举个例子:
var flage = 1;
if( flage ){
var innerValue = 111;
}
console.log(innerValue);
上面的代码不会报错,而会输出111。因为innerValue实际上是一个全局变量。它等价于下面的代码:
var flage;
var innerValue;
flage = 1;
if( flage ){
innerValue = 111;
}
console.log(innerValue);
分析题目
现在,再回过头来看题目。就很好理解了,它实际运行时是下面的样子:
var value1
var value2;
var value3;
var i;
var i2;
value1 = 0;
value1 = 0;
value1 = 0;
for( i = 10; i > 0; i-- ){
i2 = i;
(function(){
var i3 = i;
setTimeout(function(){
value1 += i;
value2 += i2;
value3 += i3;
}, 1);
})();
}
setTimeout(function(){
console.log(value1, value2, value3);
}, 100);
变量i和i2,实际上是全局变量。 最后一次执行for循环时,很显然此时i的值是1,i2最终被赋值为1。 代码继续运行,i变成0,不满足循环条件,退出循环,i最终为0。 对于i3来说,它是一个函数内部变量,最终形成了一个闭包。每一次运行匿名函数都是一个新的变量,他的值则很正常的依次被赋予10 9 8 7 .... 1。 循环结束。开始执行timeout,value1被加上十个i (0),最终为0。 value2被加上十个i2 (1), 最终为10。 value3依次加上各自私有个i3,最终为55。 所以输出是 0 10 55。
题外话
为了解决刚才所说的var两个“缺陷”,es6引入了let。let 不允许在声明之前使用,有块级作用域。将题目中var 换为let之后, 程序将输出"不诡异"的结果:55 55 55