for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(i) }, i * 1000) }
上面這個內(nèi)容會打印什么?
看過這題的都會知道答案,每隔一秒打印一個5,打印5次。如果我想將每一輪循環(huán)的i打印出來呢,很簡單,將var替換成let;
這道題真的是考察閉包嗎?
為什么要有閉包?
因為在JavaScript中,沒有辦法在函數(shù)外部訪問到函數(shù)內(nèi)部的變量對象。那么反之,有了閉包,我們可以在函數(shù)以外的任何地方訪問到函數(shù)內(nèi)部的變量對象。
(注意,我這里用的是變量對象,而不是某個變量,因為它是一個合集,準(zhǔn)確的說,是包含了整個函數(shù)作用域。)
如何寫閉包?
常見的閉包方式是:
function fn1() { var a = 1, b = 2; return function() { return a } } var fn2 = fn1(); fn2(); // 1
這里fn1執(zhí)行完成后,按理說,內(nèi)部的a、b所在的作用域應(yīng)該會銷毀,但是因為閉包的存在,返回的匿名函數(shù)保留了對當(dāng)前作用域的引用,因此我們可以在fn1執(zhí)行完成之后,依然可以訪問到fn1內(nèi)部的變量a,這就是閉包的使用。
(注意,這里雖然只是return了a,但是變量b也在內(nèi)存中,也沒有銷毀,因為閉包保存的不是某個變量,而是整個變量對象)
再來看一些其它閉包例子
function fn1() { var a = 1; setTimeout(function() { console.log(a) }, 1000 ) } fn1(); // 1
當(dāng)fn1執(zhí)行完成后,內(nèi)部作用域并沒有銷毀,而是被setTimeout保留下來了,因此這也是閉包!
var a = 1, b = 2; function () {} ..... var btn = document.getElementById('btn'); btn.addEventListener('click', function() {}, false);
沒錯,這也是閉包!我用DOM2級方式給btn這個dom節(jié)點添加事件,盡管里面什么變量都沒有引入,但依然保留著外界的變量對象,這也是閉包!
除了上面這些,還有嗎?當(dāng)然有了,比如每一個帶callback回調(diào)函數(shù)的,都是用了閉包,再比如每一個模塊導(dǎo)出的時候,一定會有閉包來訪問一些內(nèi)部的函數(shù)或者變量,這也是閉包!
好了,現(xiàn)在我懂了
那我們再來回看最初提的那個問題,思考一下
為什么原題中的代碼沒有達(dá)到我們期待的效果?
我們所期待的是,每一次for循環(huán),我們都能保存一個i的副本,將它保留下來并傳給setTimeout,我們每次循環(huán)都會重新定義這個函數(shù),也就是說第一次循環(huán)和第二次循環(huán)中的setTimeout是不一樣的(也就是說循環(huán)結(jié)束的時候,是有5個函數(shù))。題中的代碼也就等同于下面的代碼:
for (var i = 0; i < 5; i++) { { setTimeout(function() { console.log(i) }, i * 1000) } }
setTimeout本身就是一個閉包,而且大括號提供了一個塊級作用域,所以我們理想情況下很容易做到,但是卻失敗了,原因是什么?并不是閉包的問題,而是我們保存的這個i的副本,出了問題。它們都被封閉在一個共享的全局作用域中,實際上只有一個i,看似有了塊級作用域,但是沒起作用,因為是var聲明的變量不存在塊級作用域,因此循環(huán)結(jié)束的時候,“所有”的i,其實也就是一個i,就是5。
這道題的解題思路是什么?
其實就是讓var聲明的變量i保留在塊級作用域內(nèi)。
那么我們再來看,為什么用let能解決這個問題,很簡單,let聲明的變量有塊級作用域,因此i有了5個副本,并且毫不相關(guān),再配合setTimeout的閉包,我們成功了!
上面那個方法也等于下面這個
for (var i = 0; i < 5; i++) { { let j = i; setTimeout(function() { console.log(j) }, j * 1000) } }
還有沒有別的方法了,如果不改變var,如何制造塊級作用域?es5里雖然沒有塊級作用域,但是我們有模擬塊級作用域的方法:函數(shù)作用域!
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,當(dāng)然了,也可以let a ;
但是這樣寫又有些繁瑣,因為還要創(chuàng)建一個函數(shù)a,然后再銷毀,那能否不這樣呢?
IIFE!也就是立即執(zhí)行函數(shù)。
for (var i = 0; i < 5; i++) { (function(j) { setTimeout(function() { console.log(j) }, j * 1000) })(i) }
綜合來看,這道題與其說是考閉包,不如說是考塊級作用域的概念,如果硬要考閉包,不如不給代碼,把需求告訴他,讓他手寫一個,這樣才行吧。
如果有同學(xué)對作用域的概念不是很清楚,或者不是很深入,可以參考筆者另一篇文章:
對了,這里再補充一點之前提過的,當(dāng)我用let替換var的時候,既然每次循環(huán)都是一個塊級作用域,互相不干擾,那為什么i會一直自動加1呢,它是怎么記得上次循環(huán)是多少呢?
因為JavaScript引擎內(nèi)部會記住上一輪循環(huán)的值,初始化本輪的變量i時,就在上一輪循環(huán)的基礎(chǔ)上進(jìn)行計算。
以上就是今天分享的內(nèi)容了,感興趣的同學(xué)可以留言一起討論哈!
文字免費,但碼字不易,記得點贊!
感興趣可關(guān)注一波,謝謝!
浙公網(wǎng)安備 33010602011771號