最近有一道頗有意思的前端面試題前端
for (var i = 1; i <= 5; i++) { setTimeout( function timer() { console.log(i); }, i * 1000 ); } //要求改動上述代碼,使其依次輸出一、二、三、四、5
這道題涉及到的知識點有函數的執行順序
、閉包
、塊級做用域
等。面試
首先,咱們能夠來看一下這道題本來會輸出的結果是什麼數據結構
如圖,執行這段代碼以後,當即輸出了一個數字89,而後每過1秒鐘輸出了一個數字6閉包
在咱們學習setTimeout
的時候就知道,setTimeout
有兩個參數,第一個參數是回調函數,第二個參數是毫秒數,表示要執行回調函數所要延遲的時間。函數
但咱們還須要知道的是,setTimeout
會返回一個Id
,即這個定時器的Id
,在上面的代碼中其實已經建立了5個定時器,可是默認只返回了最後的一個Id
,咱們能夠經過將Id
賦值給一個變量,來看到這個過程。性能
因爲方法裏面沒有return
任何東西出來,因此返回值爲undefined
。學習
經過這個定時器的Id,可使用clearTimeout(id)
方法清除掉這個定時器,這裏就再也不贅述了。spa
接下來就該討論爲何會輸出5個數字6,而不是一、二、三、四、5了。先來看一個例子3d
當setTimeout()
的毫秒數設置爲0的時候,仍然是先執行完函數調用棧中的代碼,而後當即調用定時器。這是由於,咱們的定時器都被放在了一個被稱爲隊列的數據結構中,等待上下文的可執行代碼運行完畢後,纔開始運行定時器,也就是定時器纔剛開始計時。code
例子以下:
因此在定時器的方法執行的時候,變量i已經變成了6,因此輸出的所有是6。由於5個定時器所打印出來的是同一個i變量,因此想要實現輸出不一樣的數字,就須要把每一個定時器所訪問的變量獨立起來,這就用到了JavaScript的閉包。
咱們都知道,JavaScript的變量是從外往內開放的,函數內部能夠訪問到外部的變量,可是外部沒法訪問到內部。
內部的func()
方法就造成了一個閉包,閉包用途不少,能夠很好地區分開各個做用域,避免變量的混淆,可是濫用閉包也會致使性能問題。
想要使用閉包完成文章開始的面試題,能夠經過如下的方式
for (var i = 1; i <= 5; i++) { (function(i){ setTimeout( function timer() { console.log(i); }, i * 1000 ); })(i); } //上面的代碼是標準答案,將變量i做爲參數傳到閉包中 //咱們也能夠經過做用域在函數內部把變量隔離起來 //其實,在閉包內部訪問i的時候,i就是一個常量 for (var i = 1; i <= 5; i++) { (function(){ var s = i;//把i賦值給另一個變量 setTimeout( function timer() { console.log(s); }, s * 1000 ); })(); } //固然,也能夠把setTimeout的回調函數作成一個閉包,一樣能獲得正確的結果。
使用閉包能夠獲得正確的結果,緣由就是改變了i的做用域,那若是咱們把循環中的每一個setTimeout都獨立成一個做用域是否是也能實現一樣的結果呢?
咱們都知道,在JavaScript中,每一個函數是一個獨立的做用域,可是「{}」是不能造成獨立做用域的。
在ES6中提出了一個新的關鍵字let
,就能夠聲明一個僅對當前「{}」
內部有做用的變量。
如圖,一樣能夠實現。
這個面試題考察了setTimeout方法的原理,間接涉及了函數調用棧。利用閉包或塊級做用域能夠實現想要的效果。