L4: Understanding Closures 了解閉包 == ###### tags: `JavaScript` 在函數中放入另一個函數,先呼叫greet("Hi"),在接著執行它回傳的函數,並加入參數"Tony",其實就是閉包 ```javascript! function greet(whattosay) { return function(name) { console.log(whattosay + ' ' + name) } } greet("Hi")("Tony") // Hi Tony ``` 換個方式說明: ```javascript! function greet(whattosay) { return function(name) { console.log(whattosay + ' ' + name) } } var sayHi = greet('Hi') sayHi('Tony') // Hi Tony ``` `var sayHi = greet('Hi')`會得到回傳的函示,接著再執行`sayHi('Tony')`,會得到結果是 Hi Tony 但為什麼 `sayHi` 函數仍然知道 whattosay 是什麼? 照理來說 whattosay 變數被創造在function greet, 當greet函數被呼叫的時候,這個函數就結束了,完成執行,離開執行堆,但呼叫sayHi後,whattosay還是正確的,為什麼? ==因為是閉包== ### <font color="#3733FF">過程發生了什麼</font> 1. 執行程式碼,創造全域執行環境 ![](https://i.imgur.com/mHhYFuu.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 2. 執行`var sayHi = greet('Hi')`,新的執行環境greet被創造 ![](https://i.imgur.com/3O6QBN1.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 3. 變數whattosay 被傳入到他的變數環境裡,這時會立刻創造函數物件並回傳函數 ```javascript! return function(name) { console.log(whattosay + ' ' + name) } ``` ![](https://i.imgur.com/0i3Pnu1.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 4. 回傳函數後,greet的執行環境就離開執行堆,每個執行環境都有自己的記憶體空間,一般情形下,執行環境消失後,記憶體空間會被清除,稱為garbage collection,但在閉包裡面不會被清除,當執行環境消失後,記憶體空間的東西還在 ![](https://i.imgur.com/LUPWg33.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 5. 回到全域執行環境中,執行`sayHi('Tony')`,在回傳時沒有給函數名稱,所以是匿名函數 ![](https://i.imgur.com/sfpfv9J.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 6. sayHi 創造新的執行環境,傳入變數名稱 Tony ![](https://i.imgur.com/VE8V4fc.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 7. 執行到`console.log(whattosay + ' ' + name)`時,JS會回到scope chain去找whattosay,即使greet函數的執行環境已經離開執行堆了, sayHi的執行環境仍然可以參考到,在外部環境的記憶體空間的whattosay變數 ![](https://i.imgur.com/qNFWn0j.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> --- 這個包住所有可以取用的變數的現象 稱為==閉包==, 雖然外層執行完的函數 greet 已經不在執行堆了,執行環境可以把它的外部變數關住、包住,那些他應該要參考到的變數 這是JS程式語言的特色,這只是一個功能,確保當你執行函數時,它會正常運作 可以取用到外部變數 它不在乎外部函數已經運行完畢了沒 ### <font color="#3733FF">經典範例</font> 建立函數buildFuctions,在裡面創造一個空陣列和for迴圈,只要i小於3,就會繼續 所以會重複執行3次,然後在迴圈內,在陣列中增加東西,`arr.push( function() {console.log(i)}`,呼叫buildFunctions函數拿到array,我們期望看到console.log出現什麼? ```javascript! function buildFunctions() { var arr = [] for( var i = 0; i < 3; i++) { arr.push( function() { console.log(i) } ) } return arr } var fs = buildFunctions() fs[0]() fs[1]() fs[2]() ``` 答案是 : 3、3、3,一般初學者都會出乎意料的答案,但為什麼是3、3、3呢? ### <font color="#3733FF">過程發生了什麼</font> 1. 全域執行環境創造,有buildFunctions()和變數 fs,接著執行`var fs = buildFunctions()`,我們會有個另外的執行環境,裡面有兩個變數,在for迴圈創造的 i 和一開始宣告的arr ![](https://i.imgur.com/ZEZx1Gd.png =80%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 2. 這時候,return arr 回傳時,兩個變數的值會是什麼,第一個循環 i = 0, arr.push是創造一個新的函數,並不會執行`console.log(i)`,因為還沒有呼叫函數,接著,第二個循環 i = 1 ,再新增了另一個函數物件到陣列裡,最後,第三個循環 i = 2,陣列又被放進一個函數物件,總共有三個函數,i++ 執行後,i = 3 ,不符合for迴圈的執行條件,離開 for迴圈。 3. 當執行 return arr 時, i = 3 ,陣列裡面有三個函數(f0、f1、f2),回到全域環境,buildFunctions的執行環境離開執行堆了,但記憶體中仍然存在 i 和 arr ![](https://i.imgur.com/LFDVqAT.png =50%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 4. 接著執行 `fs[0]()` -> `fs[1]()` -> `fs[2]()`,都會指向一樣的位置 i = 3 ,他只會告訴我們現在記憶體中的值是多少,而不是我們創造函數時候的值 ![](https://i.imgur.com/mMXlpM7.png =33%x)![](https://i.imgur.com/7ifkMoG.png =33%x)![](https://i.imgur.com/XIwcI4s.png =33%x) <font color="#999999">圖片來源:udemy:JavaScript 全攻略:克服 JS 的奇怪部分 </font> 5. 一開始這看起來可能很怪 但當你瞭解之後,==console.log不是在他所在的地方執行,而是當我們呼叫函數才執行== ### 調整成印出的值是 0、1、2 在ES6 ,可以直接把var 改成 let 就好 ```javascript! function buildFunctions2() { var arr = [] for( let i = 0; i < 3; i++) { arr.push( function() { console.log(i) } ) } return arr } ``` 如果是ES5 , 用閉包+IIFE的方式 為了保存函數裡的 i 值,需要建立不同的執行環境,放進陣列裡,即使這些執行環境在執行後就結束,因為有閉包,所有的 j 在這三個不同執行環境,都會被記憶體記住 ```javascript! function buildFunctions() { var arr = [] for( var i = 0; i < 3; i++) { arr.push( (function(j) { return function() { console.log(j) } }(i)) ) } return arr } ``` 執行push時,會把函數的執行結果`function() {console.log(j)}` 回傳給我們,push到array裡 ```javascript! (function(j) { return function() { console.log(j) } }(i)) ``` 然後當`function() {console.log(j)}`被執行,會尋找j的值,j 的值會在迴圈執行時儲存 這是利用閉包的方法,來確保在`fs[0]()`呼叫時 執行內部函數會是正確的值 --- 閉包的好處,範例 寫一個makeGreeting函數,裡面有一個匿名函數,可以根據不同的language印出不同的字,每一次函數被呼叫都會產生新的執行環境,所以當我們建立兩個變數能存放,放入不同的參數的結果,執行後得到的答案就不同,我們不需要每次都傳入一樣的參數,可以創造新的函數,用閉包製造預設的參數。 有點像是工廠一樣,模具都準備好了,只要是有預先定義的就可以提供不同的值。兩者之間也不會互相影響。 ```javascript! function makeGreeting(language) { return function (firstname, lastname) { if (language === 'en') { console.log('Hello ' + firstname + ' ' + lastname) } if (language === 'es') { console.log('Hola ' + firstname + ' ' + lastname) } } } const greetEnglish = makeGreeting('en') const greetSpanish = makeGreeting('es') // console.log(greetEnglish) // console.log(greetSpanish) greetEnglish('John','Doe') greetSpanish('John','Doe') ``` --- ### 閉包 題目: #### Q1: 下列程式碼,執行到 console.log(add5(2)) 的時候,回傳值會是多少?請你解釋過程中的資料如何傳遞,以及參數 x 和 y 的值如何變化。 ```javascript function makeAdder(x) { return function(y) { return x + y } } var add5 = makeAdder(5) console.log(add5(2)) ``` console.log(add5(2)) 的值會是7 函數makeAdder裡面還會回傳一個匿名函數,這是一個閉包,可以訪問外部函數makeAdder 的作用域 特性是變數會被保留,當執行 var add5 = makeAdder(5),傳入參數5當作x的值,且會得到回傳的函數 `return function(y) { return x + y }` 接著執行console.log(add5(2)),代表要執行匿名函數,傳入2作為y的參數,這時x的值,JS engine會到外部去找x,在記憶體中是2,所以才會是2+5=7