最近一道面試試題很是火熱,堪稱面試界網紅: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值的正確引用。
以上就是本身對這個網紅面試題的深刻理解,若是有說的有錯或模棱兩可的地方,還請不吝指教。線程