詳解setTimeOut面試題

最近有一道頗有意思的前端面試題前端

for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
      console.log(i);
  }, i * 1000 );
}

//要求改動上述代碼,使其依次輸出一、二、三、四、5

這道題涉及到的知識點有函數的執行順序閉包塊級做用域等。面試

首先,咱們能夠來看一下這道題本來會輸出的結果是什麼數據結構

clipboard.png

如圖,執行這段代碼以後,當即輸出了一個數字89,而後每過1秒鐘輸出了一個數字6閉包

setTimeout()相關知識

在咱們學習setTimeout的時候就知道,setTimeout有兩個參數,第一個參數是回調函數,第二個參數是毫秒數,表示要執行回調函數所要延遲的時間。函數

但咱們還須要知道的是,setTimeout會返回一個Id,即這個定時器的Id,在上面的代碼中其實已經建立了5個定時器,可是默認只返回了最後的一個Id,咱們能夠經過將Id賦值給一個變量,來看到這個過程。性能

clipboard.png

因爲方法裏面沒有return任何東西出來,因此返回值爲undefined學習

經過這個定時器的Id,可使用clearTimeout(id)方法清除掉這個定時器,這裏就再也不贅述了。spa

接下來就該討論爲何會輸出5個數字6,而不是一、二、三、四、5了。先來看一個例子3d

clipboard.png

setTimeout()的毫秒數設置爲0的時候,仍然是先執行完函數調用棧中的代碼,而後當即調用定時器。這是由於,咱們的定時器都被放在了一個被稱爲隊列的數據結構中,等待上下文的可執行代碼運行完畢後,纔開始運行定時器,也就是定時器纔剛開始計時。code

clipboard.png

例子以下:

clipboard.png

因此在定時器的方法執行的時候,變量i已經變成了6,因此輸出的所有是6。由於5個定時器所打印出來的是同一個i變量,因此想要實現輸出不一樣的數字,就須要把每一個定時器所訪問的變量獨立起來,這就用到了JavaScript的閉包。

做用域和閉包

咱們都知道,JavaScript的變量是從外往內開放的,函數內部能夠訪問到外部的變量,可是外部沒法訪問到內部。

clipboard.png

內部的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的回調函數作成一個閉包,一樣能獲得正確的結果。

塊級做用域--關鍵字let

使用閉包能夠獲得正確的結果,緣由就是改變了i的做用域,那若是咱們把循環中的每一個setTimeout都獨立成一個做用域是否是也能實現一樣的結果呢?

咱們都知道,在JavaScript中,每一個函數是一個獨立的做用域,可是「{}」是不能造成獨立做用域的。

在ES6中提出了一個新的關鍵字let,就能夠聲明一個僅對當前「{}」內部有做用的變量。

clipboard.png

如圖,一樣能夠實現。

總結

這個面試題考察了setTimeout方法的原理,間接涉及了函數調用棧。利用閉包或塊級做用域能夠實現想要的效果。

圖片描述

相關文章
相關標籤/搜索