最近一道面试试题很是火热,堪称面试界网红:javascript
function test(){ for (var i=0; i<5; i++) { setTimeout( function timer() { console.log(new Date(),i); }, 1000*i ); } console.log("end",new Date(),i); //为方便后边演示,这里加了打印end标志 }
不理解闭包,变量做用域和setTimeout函数的同窗不少会给出答案A:0,1,2,3,4,5和答案B:5,0,1,2,3,4;不奇怪,但正确答案倒是5,5,5,5,5,且是隔一秒出来一个。固然相比较,说出答案B至少比答案A多知道setTimeout函数的用法,重点不在那个延迟1000*i ms,重点在setTimeout函数与做用域链,后面会细说。
首先三个概念:
setTimeout(code,millisec)函数:用于在指定的毫秒数后调用函数或计算表达式,接受两个参数,第一个参数为一个函数或计算表达式,咱们经过该函数定义将要执行的操做。第二个参数为一个时间毫秒数,表示延迟执行的时间。至于什么异步调用,队列这些概念,这里不作详述,可阅读:http://www.alloyteam.com/2015...
函数做用域:函数内部定义的变量与外部定义的变量,外部指包含这个函数的空间,是父子关系,二不是兄弟,编过程的应该都理解;不管函数在哪里被调用,也不管它如何被调用,它的词法做用域都只由函数被声明时所处的位置决定;
闭包:闭包(Closure)是词法闭包(Lexical Closure)的简称,是引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即便已经离开了创造它的环境也不例外。详细运用,推荐读:http://www.ruanyifeng.com/blo...,我的仍是推荐红宝书上面的讲解。
等明白上面第一个settimeout概念后,最后一行为何先打印最后一行的结果了;
明白变量做用域后,就会明白console.log("end",new Date(),i)中的i是for循环声明的那个i变量,由于var声明的变量不存在代码块({})做用域的概念,因此最后打印的值是5;
明白函数后,和变量做用域一块儿理解,咱们能够得出相似以下所示的图(若是理解不正确,还请大神指正)
在for循环声明的五个TimeOut Callback函数都有对变量i的引用(这里的引用不是引用类型的引用,而是i做为函数做用域链的一个变量,因为闭包形成的),而不是拷贝。由于5个timeout函数都涉及到延迟执行的状况,因此当主线程执行完后(end被打印时),timeout这些回调依次执行(队列:FIFO),此时i的值已经为5了,知道以上这些,后面就简单多了。html
开始回到正题:
其实写出这个函数指望输出5,0,1,2,3,4,要达到这个结果,方法有多种,这里列出典型的三种:
方法1:IIFE:java
function test(){ for (var i = 0; i < 5; i++) { (function(j) { // j = i setTimeout(function() { console.log(new Date, j); }, 1000*j); })(i); } console.log(new Date, i); }
方法2:函数调用按值传递:面试
var output = function (i) { setTimeout(function() { console.log(new Date, i); }, 1000); }; function test(){ for (var i = 0; i < 5; i++) { output(i); // 这里传过去的 i 值被复制,而不是引用 } console.log(new Date, i); }
方法2:函数调用按值传递技巧版(利用setTimeout第三个参数):闭包
function test(){ for (var i = 0; i < 5; i++) { setTimeout(function(i) { console.log(new Date, i); }, 1000, i); } console.log(new Date, i); }
方法3: ES6 使用le指令声明:异步
function test(){ for (let i=0; i<5; i++) { setTimeout( function timer() { console.log(new Date(),i); }, 1000 ); } // console.log("end",new Date(),i); //由于变量做用域的问题,这里会报i 不存在,未声明 }
细度上面的三种方法,其实他们类似度很高。首先方法1(声明即执行)和方法2(提早声明,调用时执行),其实他们的思路彻底一致,都利用了JavaSrcipt中函数基本类型变量传值,都是值的拷贝,而不是值的引用,而后经过在for循环中执行一个闭包函数,创建一个闭包做用域,来保证引用的i值为注册该回调函数时的值。当即即执行,若是看着别扭,下面这样写也是能够的:函数
function test(){ for (var i = 0; i < 5; i++) { (function() { // j = i var j =i; setTimeout(function() { console.log(new Date, j); }, 1000); })(); } console.log(new Date, i); }
简化版:oop
function test(){ for (var i = 0; i < 5; i++) { (function(j) { // j = i setTimeout(function() { return function(){ console.log(new Date, j); } }, 1000); })(i); } console.log(new Date, i); }
而后方法3,是利用ES6 let命令声明变量块级做用域的概念,和前面for循环使用var声明i不一样的是,var声明的i在整个test()函数做用域内有效,每一次循环, 新的i值都会覆盖旧值;而let声明的, 当前的i只在本轮循环有效, 因此每一次循环的i其实都是一个新的变量,因此也致使打印end时,报i 不存在,未声明的错误,这就是块级做用域的效果,因此5个timeout回调函数虽然都引用了变量i,但实际上这5个i是独立的,仅在本身的块级做用域内有效,其写法相似于:spa
function test(){ for (var i=0; i<5; i++) { let j =i; setTimeout( function timer() { console.log(new Date(),j); }, 1000 ); } console.log("end",new Date(),i); }
因此整体来看,上面的方法解决的思路都是从做用域这个概念上下手的,前二者利用function声明造成了本身的做用域,后者利用let命令造成的块级做用域,而来确保对i值的正确引用。
以上就是本身对这个网红面试题的深刻理解,若是有说的有错或模棱两可的地方,还请不吝指教。线程