--- tags: JavaScript title: 閉包 --- # 閉包 閉包為範圍鏈、記憶體空間、表達式以及函式等觀念總合的概念,重複執行函式時,其變數值就會累加上去。要利用閉包的概念前,先來複習一下函式還有怎樣的特性。 - 函式與範圍鏈 - 函式與提升 - 函式與記憶體空間 ## 函式與範圍鏈 在函式中的變數,若沒有宣告,則往外層尋找直到全域,若全域也沒有,則報錯。 ### 靜態作用域 跟在哪裡被呼叫無關,跟位置有關。 來看以下範例,函式的位置會影響最後的結果。 ```javascript var a = 1 function fnA(){ console.log(a) } function fnB(){ var a = 2 fnA() } fnB() // 1 // fnA()內本身沒有 a變數,則向外層找 // 它也不在 fnB()內,於是找到了全域物件中的變數 a function fnC(){ var a = 3 function fnD(){ console.log(a) } fnD() } fnC() // 3 // fnD()本身沒有 a變數,則向外層找 // 在 fnC()找到已宣告的變數 a ``` ### 動態作用域 跟在哪裡使用無關,跟在哪裡被呼叫有關,最常見的就是 `this` 的運用。 ## 函式與提升(Hoisting) 在函式中 : - 將自訂參數重新宣告不會有任何作用 - 函式中的函式,不會提升至外層 - 若在函式中不將變數宣告或者非自訂參數,則可能依據呼叫環境在全域物件中建立變數 ```javascript function fn(a){ console.log(a) // 將自訂參數重新宣告不會有任何作用 var a console.log(a) // 賦予新值 a = 1 console.log(a) // 若在裏頭插入函式,它不會提升到外層 function fnB(){ console.log(a) } fnB() b = 2 } fn('我是函式') console.log(a) console.log(b) // '我是函式' // '我是函式' // 1 // 1 // a is not defined // 2,b未正式宣告且因為範圍鏈(向外層尋找)的關係,在全域物件中建立了變數 ``` ## 函式與記憶體空間 先看下面的例子,函式每次執行時,都會加 1。 ```javascript var a = 0 function fn(){ a += 1 console.log(a) } fn() // 1 fn() // 2 fn() // 3 fn() // 4 fn() // 5 ``` 如果將變數 a,移動到函式內,重複執行函式時,就會因為記憶體空間被釋放掉,又重新宣告而無法累計。 ```javascript function fn(){ var a = 0 a += 1 console.log(a) } fn() // 1 fn() // 1 fn() // 1 fn() // 1 fn() // 1 ``` 接下來,改寫上述範例,假設今天去夜市套圈圈,老闆的每個籃子裡總共有五個圈圈,規則是每次只能丟一個圈圈。今天有兩個玩家,都另開新局,他們都能有五個圈圈可投。 - return 一個函式,此函式包含著變數,該變數會視為還須利用而不會被上層的函式釋放掉 - 分別宣告變數並賦予函式,函式內部所宣告的變數不會互相影響 ```javascript function game(){ var times = 5 return function(){ times -= 1 return `圈圈剩餘 ${times} 個` } } var personA = game() console.log(personA()) // 圈圈剩餘 4個 console.log(personA()) // 圈圈剩餘 3個 console.log(personA()) // 圈圈剩餘 2個 console.log(personA()) // 圈圈剩餘 1個 console.log(personA()) // 圈圈剩餘 0個 var personB = game() console.log(personB()) // 圈圈剩餘 4個 console.log(personB()) // 圈圈剩餘 3個 console.log(personB()) // 圈圈剩餘 2個 console.log(personB()) // 圈圈剩餘 1個 console.log(personB()) // 圈圈剩餘 0個 ``` ### 閉包與函式工廠 這次老闆使用機器人自動丟五個圈圈出去,每次丟一個就紀錄一次剩餘多少個圈圈。 ```javascript function game(){ var times = 5 var gameLog = [] for (var x = 0 ; x < 5 ; x++){ times -= 1 gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`) } return gameLog } var personA = game() console.log(personA) // ["第 1 次丟出圈圈剩餘 4 個", // "第 2 次丟出圈圈剩餘 3 個", // "第 3 次丟出圈圈剩餘 2 個", // "第 4 次丟出圈圈剩餘 1 個", // "第 5 次丟出圈圈剩餘 0 個"] ``` ### 閉包的私有方法 讓我們再度強化機器人的可用性,基礎次數五次,每丟一次就記錄一次到陣列中,利用閉包的原理,將函式 return一個物件。 但是,這台機器人卻有個地方無法正常顯示,該怎麼修復它呢? ```javascript function game(){ var times = 5 var gameLog = [] return { log: gameLog, nowTimes: `目前剩餘次數為 ${times} 次`, addTimes: function(number){ times += number return `目前剩餘次數為 ${times} 次` }, start: function(){ for (var x = 0 ; x < 5 ; x++){ times -= 1 gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`) } return '開始自動執行遊戲' } } } var personA = game() console.log(personA.addTimes(25)) // 30 console.log(personA.nowTimes) // 卻還是顯示 5次 ``` 函式 `personA` return的物件,其包含許多函式與變數,而單純變數值卻無法修改? 我們來檢查看看, ```javascript // 節錄 personA片段 console.log(personA) // Object { // nowTimes: `目前剩餘次數為 5 次`, // addTimes: function(number){ // times += number // return `目前剩餘次數為 ${times} 次` // }, // } ``` 可以看到有的直接變成字串,而待在函式裡的變數則維持變數的樣子,讓我們再回想閉包的原理,若該函式內的變數有需要用到,則記憶體空間不會被釋放掉,也就是說,屬性值為單純的變數,被使用後就釋放掉了,待在函式內的則不會。那麼,我們來修正機器人,將 `addTimes` 這個屬型值改為函式,這樣就能正確顯示了。 ```javascript function game(){ var times = 5 var gameLog = [] return { log: gameLog, nowTimes: function(){ return `目前剩餘次數為 ${times} 次` }, addTimes: function(number){ times += number return `目前剩餘次數為 ${times} 次` }, start: function(){ for (var x = 0 ; x < 5 ; x++){ times -= 1 gameLog.push(`第 ${x+1} 次丟出圈圈剩餘 ${times} 個`) } return '開始自動執行遊戲' } } } var personA = game() console.log(personA.addTimes(25)) // 30 console.log(personA.nowTimes()) // 30 ``` ## 參考來源 > 1. [Huli - 所有的函式都是閉包:談 JS 中的作用域與 Closure](https://blog.techbridge.cc/2018/12/08/javascript-closure/) > 2. [OneJar - 你不可不知的 JavaScript 二三事#Day5:湯姆克魯斯與唐家霸王槍—變數的作用域(Scope) (1)](https://ithelp.ithome.com.tw/articles/10203387)