從一道經典前端面試題再來看閉包
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