先看一条面试题:
for (var i = 1; i <= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 );
}
console.log('end i ',i);
//输出结果:end i 6 -> 6 -> 6 -> 6 -> 6 -> 6
//造成后面输出都是6,我的简单理解是因为setTimeout是异步执行(循环结束后),
//涉及到延迟执行的情况,所以当主线程执行完后(end被打印时),
//等到循环结束i最终变为6,并且i是全局变量,所以setTimout内部函数读到全局变量i是6。
//现在的要求是改动上述代码,使其依次输出1、2、3、4、5
//这道题涉及到的知识点(函数的执行顺序、闭包、块级作用域)
先过一遍概念
- setTimeout(code,millisec) : 用于在指定的毫秒数后调用函数或计算表达式。当setTimeout()的毫秒数设置为0的时候,仍然是先执行完函数调用栈中的代码,然后立即调用定时器。所以在定时器的方法执行的时候,变量i已经变成了6,所以输出的全部是6
- 函数作用域 :JavaScript的变量是从外往内开放的,函数内部可以访问到外部的变量,但是外部无法访问到内部。
- 闭包:变量的作用域无非就是两种:全局变量和局部变量。函数内部可以直接读取全局变量,函数外部无法读取函数内的局部变量。闭包就是能够读取其他函数内部变量的函数。【只有函数内部的子函数才能读取局部变量,因此可以把闭包简单理解成"定义在一个函数内部的函数"】
方法一:用IIFE(立即调用函数表达式)修改代码,使它输出1,2,3,4,5
for (var i = 1; i <= 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
console.log('end i ',i);
//输出结果:end i 6 -> 1 -> 2 -> 3 -> 4 -> 5
//分析:输出将变量i作为参数传到闭包中,在闭包内部访问i的时候,i就是一个常量
方法二: ES6 使用le指令声明
for (let i = 1; i<= 5; i++) {
setTimeout( function timer() {
console.log(i);
}, i * 1000 );
}
console.log("end i ",i); //因为变量作用域的问题,这里会报i不存在,未声明
//输出结果:Uncaught ReferenceError: i is not defined -> 1 -> 2 -> 3 -> 4 -> 5
//分析:主要是因为ES6的let命令声明变量块级作用域,对比之前的for循环用var声明,
//区别在于var声明的i是全局变量,并且每循环一次,i都会被新值覆盖,
//所以最终i输出的是循环后结果。But,let声明的只在循环函数体内有效,
//所以每次循环的i都是新变量,所以setTimout函数都引用了新变量。
总结:这个题目的重点是改变i的作用域,我们可以通过闭包的方法改变作用域,也可利用let命令形成的块级作用域,来确保对i值的正确引用。