1. 程式人生 > >從一道經典前端面試題再來看閉包

從一道經典前端面試題再來看閉包

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

上面這個內容會列印什麼?

看過這題的都會知道答案,每隔一秒列印一個5,列印5次。如果我想將每一輪迴圈的i打印出來呢,很簡單,將var替換成let;

這道題真的是考察閉包嗎?

 

為什麼要有閉包?

因為在JavaScript中,沒有辦法在函式外部訪問到函式內部的變數物件。那麼反之,有了閉包,我們可以在函式以外的任何地方訪問到函式內部的變數物件。

(注意,我這裡用的是變數物件,而不是某個變數,因為它是一個合集,準確的說,是包含了整個函式作用域。)

 

如何寫閉包?

常見的閉包方式是:

function fn1() {
  var a = 1,
        b = 2;
  return function() {
       return a
  }    
}

var fn2 = fn1();
fn2();    // 1

這裡fn1執行完成後,按理說,內部的a、b所在的作用域應該會銷燬,但是因為閉包的存在,返回的匿名函式保留了對當前作用域的引用,因此我們可以在fn1執行完成之後,依然可以訪問到fn1內部的變數a,這就是閉包的使用。

(注意,這裡雖然只是return了a,但是變數b也在記憶體中,也沒有銷燬,因為閉包儲存的不是某個變數,而是整個變數物件)

 

再來看一些其它閉包例子

function fn1() {
  var a = 1;  
  setTimeout(function() {
      console.log(a)  
  }, 1000 )  
}

fn1();

// 1

當fn1執行完成後,內部作用域並沒有銷燬,而是被setTimeout保留下來了,因此這也是閉包!

 

var a = 1, b = 2;

function () {}

.....

var btn = document.getElementById('btn');

btn.addEventListener('click', function() {}, false);

 

沒錯,這也是閉包!我用DOM2級方式給btn這個dom節點新增事件,儘管裡面什麼變數都沒有引入,但依然保留著外界的變數物件,這也是閉包!

 

除了上面這些,還有嗎?當然有了,比如每一個帶callback回撥函式的,都是用了閉包,再比如每一個模組匯出的時候,一定會有閉包來訪問一些內部的函式或者變數,這也是閉包!

 

好了,現在我懂了

 

那我們再來回看最初提的那個問題,思考一下

 

為什麼原題中的程式碼沒有達到我們期待的效果?

我們所期待的是,每一次for迴圈,我們都能儲存一個i的副本,將它保留下來並傳給setTimeout,我們每次迴圈都會重新定義這個函式,也就是說第一次迴圈和第二次迴圈中的setTimeout是不一樣的(也就是說迴圈結束的時候,是有5個函式)。題中的程式碼也就等同於下面的程式碼:

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

setTimeout本身就是一個閉包,而且大括號提供了一個塊級作用域,所以我們理想情況下很容易做到,但是卻失敗了,原因是什麼?並不是閉包的問題,而是我們儲存的這個i的副本,出了問題。它們都被封閉在一個共享的全域性作用域中,實際上只有一個i,看似有了塊級作用域,但是沒起作用,因為是var宣告的變數不存在塊級作用域,因此迴圈結束的時候,“所有”的i,其實也就是一個i,就是5。

 

這道題的解題思路是什麼?

其實就是讓var宣告的變數i保留在塊級作用域內。

 

那麼我們再來看,為什麼用let能解決這個問題,很簡單,let宣告的變數有塊級作用域,因此i有了5個副本,並且毫不相關,再配合setTimeout的閉包,我們成功了!

上面那個方法也等於下面這個

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

 

還有沒有別的方法了,如果不改變var,如何製造塊級作用域?es5裡雖然沒有塊級作用域,但是我們有模擬塊級作用域的方法:函式作用域!

for (var i = 0; i < 5; i++) {
  var a = function(j) {
      setTimeout(function() {
         console.log(j)   
      }, j * 1000)  
  };
   a(i);
   a = null;
} 

這裡為了避免變數a汙染全域性,最後將a賦值為null,當然了,也可以let a ;

但是這樣寫又有些繁瑣,因為還要建立一個函式a,然後再銷燬,那能否不這樣呢?

 

IIFE!也就是立即執行函式。

 

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

 

綜合來看,這道題與其說是考閉包,不如說是考塊級作用域的概念,如果硬要考閉包,不如不給程式碼,把需求告訴他,讓他手寫一個,這樣才行吧。

 

對了,這裡再補充一點之前提過的,當我用let替換var的時候,既然每次迴圈都是一個塊級作用域,互相不干擾,那為什麼i會一直自動加1呢,它是怎麼記得上次迴圈是多少呢?

因為JavaScript引擎內部會記住上一輪迴圈的值,初始化本輪的變數i時,就在上一輪迴圈的基礎上進行計算。

 

end