在瞭解閉包以前,咱們要清楚一點。咱們瞭解閉包,不是爲了去有意的建立閉包,實際上咱們在寫代碼的過程當中,就會無心的建立不少閉包,咱們要作的只是瞭解和熟悉,在寫代碼的時候知道寫出來的是閉包,而後在出現一些奇怪的bug的時候能正確找到它們。segmentfault
在開始前,先看一個小栗子數組
問題:每一個一秒分別打印 0、一、2閉包
解法1:異步
(function foo(){ for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); } })()
預期結果: 0、一、2
實際結果: 三、三、3
是否是很奇怪,前面這段代碼看起來是沒什麼問題啊,輸出結果怎麼會不對?
這個疑問先放一放,咱們先來看一下今天的主角,「閉包」同窗函數
直接上代碼:spa
function fn1() { var a = 2; function fn2() { console.log(a); } return fn2; } var fn3 = fn1(); fn3();
上面這段代碼中作了三件事情:
1⃣️ 函數 fn1 執行
2⃣️ fn1 的返回值(執行結果)被賦值給fn3
3⃣️ fn3(也就是fn2) 執行,打印變量 aprototype
fn1執行前,引擎先爲fn1建立了一個活動對象,而後塞進內存中。咱們知道,js引擎有垃圾回收機制,會釋放再也不使用的內存空間,等fn1執行完以後,按理說前面建立的活動對象已經沒用了,這個時候引擎會將該活動對象回收。code
可是這裏並不會,由於fn2內部引用的變量a是存活在fn1活動對象中的,也就是說fn2引用了fn1活動對象中的a,這也就使得fn1活動對象不會被銷燬,仍然存活在內存中。( 能夠理解爲:引擎準備清理fn1活動對象的時候,發現還被別的對象引用着,說明它還有用,就放棄回收它了 )對象
由於fn1活動對象不會被銷燬,等到fn3執行的時候,須要獲取a的值並打印,就能正常獲取和打印了。blog
總的來講就是,fn2持有了對fn1的引用,致使fn1執行完以後活動對象沒有被銷燬,這個現象就叫作閉包。
瞭解完閉包,如今咱們來分析一下文章開頭那段輸出結果不對的代碼:
// 原代碼: (function foo(){ for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); } })()
對上面的代碼作個拆解:
// 拆解後: (function foo(){ var i i = 0 // 第一次循環 此時 i === 0 if(i < 3){ // setTimeout 執行,定時器開啓 // 可是因爲fn1是異步代碼,因此fn1會等到全部同步代碼執行完成後再執行 setTimeout(function fn1 (){ console.log(i) }, 0); // 1000 * 0 } // i 自增 i++ // 第二次循環 此時 i === 1 if(i < 3){ setTimeout(function fn1 (){ console.log(i) }, 1000); // 1000 * 1 } i++ // 第三次循環 此時 i === 2 if(i < 3){ setTimeout(function fn1 (){ console.log(i) }, 2000); // 1000 * 2 } i++ // 這裏 i === 3 ,不知足判斷條件 i < 3 ,纔會跳出循環 })()
如今咱們再去看,for循環建立了三個定時器,每一個定時器分別有一個回調函數fn1。
仔細看這裏的每一個fn1函數中輸出的變量i都是引用的外層函數foo的,根據咱們討論閉包得出的結論,因爲函數fn1引用了外層函數foo的變量i,因此fn1持有了對外層函數foo的引用,致使了foo函數的活動對象不會被銷燬。
因此這段代碼中會產生3個閉包,關係以下圖:
三個fn1函數雖然分別產生了三個閉包,可是引用的是同一個外層函數foo的值,因此咱們能夠理解爲三個閉包都是共享的。三個函數使用的是同一個父級做用域下的變量 i ,因此異步函數fn1 執行時,獲取到的是同一個i值(i === 3)
這個栗子拆解是爲了講閉包,同時爲了方便後面其餘代碼的講解,因此用閉包的思路去分析。可是實際關鍵節點仍是異步問題,小夥伴們不要鑽牛角尖。
說到這裏,眼尖的小夥伴可能已經發現了,那既然是取的時候已是3了,那我每次進循環的時候,都把當前的i值存一下行不行?行啊,固然行,這就是咱們接下來要說的。
如今再回去看一下最開始的代碼快,而後對代碼作個改造。
for (var i = 0; i< 3; i++) { setTimeout(function fn1 (){ console.log(i) }, 1000 * i); }
經過前面的討論,咱們能夠肯定,由於這裏的fn1函數和相同的父級做用域造成了共享閉包。因此爲了解決這個問題,咱們能夠給每一個fn1函數外面再包一層做用域,拆分紅三個獨立小閉包。
改造:
for (var i = 0; i< 3; i++) { (function foo (i) { // 這裏加一層當即執行函數 setTimeout(function fn1 (){ console.log(i) }, 1000 * i); })(i) // 每次循環的時候,都給 foo 的 i 賦值 }
拆解:
var i i = 0 if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 0); // 1000 * 0 })(i) // i === 0 (⚠️:當即執行函數,因此代碼執行到這裏的時候,就已經把foo內部的i給賦值成0了) } i++ if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 1000); // 1000 * 1 })(i) // i === 1 } i++ if(i < 3){ (function foo (i) { setTimeout(function fn1 (){ console.log(i) }, 2000); // 1000 * 2 })(i) // i === 2 } i++ // 異步函數執行
關係圖:
下面的代碼和前面用自調用函數拆分閉包的道理是同樣的,區別只是把函數做用域變成了塊級做用域。
⚠️:使用let關鍵字,會隱式的建立塊級做用域
改造:
for (let i = 0; i< 3; i++) { // 這裏的 var 改爲 let setTimeout(function fn2 (){ console.log(i) }, 100); }
拆解:
var i i = 0 if(i < 3){ let j = i setTimeout(function fn1 (){ console.log(j) }, 0); } i++ if(i < 3){ let j = i setTimeout(function fn1 (){ console.log(j) }, 1000); // 1000 * 1 } i++ if(i < 3){ let j = i~~~~ setTimeout(function fn1 (){ console.log(j) }, 2000); // 1000 * 2 } i++ // 異步函數執行
關係圖:
簡單的聊一下函數柯里化,柯里化其實就是閉包的一種利用。
好比說咱們要實現這樣的一個效果:
實現一個函數,能夠不停的往裏傳string,直到傳入句號,結束並返回全部string拼接的結果。
例子:
strConcat('H') strConcat('e', 'll') strConcat('o', ' ', 'W') strConcat('o') strConcat('rl') strConcat('d','.') // 輸出 Hello Word.
實現:
function fn1() { let arr = [] // ** 重要:返回函數 concat ** return function concat() { // 拿到參數數組 const arg = Array.prototype.slice.call(arguments) // 將參數存到外層做用域下的arr中 // 因爲閉包的緣由,每次concat執行時,arr都會保持上一次操做結果 arr = arr.concat(arg) // 接到終止參數,則返回拼接字符串 if(~arg.indexOf('.')){ const result = arr.join('') console.log(result) return result } } } // ** 重要:這裏 strConcat === concat ** const strConcat = fn1() strConcat('H') strConcat('e', 'll') strConcat('o', ' ', 'W') strConcat('o') strConcat('rl') strConcat('d','.') // 輸出 Hello Word.
柯里化其實就是利用閉包的原理,實現的一個相似於一個小倉庫的效果。
咱們用包子工廠舉個栗子:
能夠看到,實際就是利用了 fn1 活動對象不會被銷燬的特色,把fn1當成了一個臨時倉庫,等全部包子原料sring加工完以後(偷懶了,個人貼的代碼裏沒有加工的過程,可是道理是同樣的),再統一輸出。
定義:詞法做用域就是定義在詞法階段的做用域。
解釋:詞法階段也就是詞法分析階段(預編譯階段)。換句話說,詞法做用域是在代碼執行前就已經肯定的做用域。
也就是說,詞法做用域在代碼執行前就被肯定了,因此詞法做用域是不會由於代碼的執行而改變的。(其實仍是有辦法改變的,好比用eval搞一些奇怪的事情,可是這不在咱們討論範圍內了,咱們就當它不變的就行了)
上面的圖中顏色深淺不一的三個地方就是三個詞法做用域,它們是徹底包含的關係,1⃣️ > 2⃣️ > 3⃣️
1⃣️ 包含foo函數所在的做用域,也就是全局做用域2⃣️ 包含foo函數所建立的做用域,也就是bar函數所在的做用域3⃣️ 包含bar函數建立的做用域