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


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM