iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 8
3
Modern Web

JavaScript 原力覺醒 - 成為絕地武士之路系列 第 8

JS 原力覺醒 Day08 - Closures

一路上感謝各位讀者們的支持和回饋。
本 30 天系列文目前已經將篇幅重新整理、編纂成冊。
《JavaScript 概念三明治》在天瓏書局上架囉!
喜歡這個系列,想閱讀更詳細原理說明的讀者可以參考:
https://www.tenlong.com.tw/products/9789864347575

Outline

  • Closure 的形成
  • 經典範例

Closure 的形成

函數內的變數在函式執行完之後,就無法再被參照到,這個時候一開始被分派的記憶體就會被釋放什麼意思呢?

function getEnemyInfo(){
	let enemies = ['Darth Vader','Sheev Palpatine'];
  let enemyLeader = 'Sheev Palpatine'
	 return enemies
}

function getBattleInfo(){
	let fellowInfo = ['Clone' , 'Clone' , 'Warship', 'Clone'];
	let enenyInfo = getEnemyInfo() 

	// enemyLeader is not defined 
  // because it's located in another function.
	console.log(enemyLeader) 

	return `the number of fellows is: ${fellowInfo.length }, and
					the number of enemies is" ${enenyInfo.length}` 
} 

getBattleInfo()

先來了解一下基本觀念,如上面這個例子,getEnemyInfo 函式裡面宣告的變數,在函式執行環境結束後(執行完),就再也無法取得,( 也可以說該變數的有效範圍只存在於該函式內 ),因為這時執行堆疊已經剩下全域。除非我把該變數宣告在全域,否則在外面是無法拿到的。

下面來看一個經典的 Closure 例子:

let country = 'United Nations'
let soilder = ['Clone' , 'Clone' , 'Warship', 'Clone']; 
let jedi = ['Yoda' , 'Obi-Wan', 'Anakin']

function addA(numA){
	return function (numB){
		return numA+numB
	} 
} 

let addB = addA(jedi.length)

let fellowNum = addB(soilder.length) 

上面是一個要把兩個數字加起來的 add 函式。 這個函式會返回另外一個函式,之後才會真正把兩個數字加起來,在我們輸入第一個參數之後,就會結束該函式執行環境並返回另外一個函式。

https://ithelp.ithome.com.tw/upload/images/20190923/201065801yPoLwCGrd.jpg

照理說當 addA 函式回傳第二個函式之後,addA 的執行環境就結束,就沒辦法拿到該函式參數 numA ,但對 addB 函式而言,在其內部有引用到他內部沒有的變數 (numA),因此他會轉為向外部環境( Scpoe Chain )尋找 ,JS 引擎就會為此保留這個函式的記憶體空間,不會釋放。

https://ithelp.ithome.com.tw/upload/images/20190923/20106580IF1A2kWGjV.jpg

這看起來就 numA 在 addB 執行環境存在時,暫時為了 addB 而被保留,完全只屬於 addB ,所以這個暫時存在的封閉環境,就被稱為「閉包 ( Closure )」。

經典範例

接下來我們要來看一個很常見,且非常容易讓人誤解的例子:

function pushFuncToArray(){
		var funcArr = [] 
		for (var i=0; i<3; i++){
			 funcArr.push(function(){
					console.log(i)
				}) 
		} 
		return funcArr
} 

var functionArr = pushFuncToArray()

functionArr[0]()
functionArr[1]()
functionArr[2]()

如果你沒有接觸過 Closure ,乍看之下一定會覺得依序的執行結果會是 0,1,2 ,可是~~瑞凡,~~並不是,結果是 3,3,3 ! 這是為什麼?很簡單,我們在把函式推到陣列裡面的時候 ,因為處於 pushFuncToArray 內部且有引用到該函式內的變數 i ,而形成閉包。

https://ithelp.ithome.com.tw/upload/images/20190923/20106580AWlAh7veMH.jpg

JS 引擎的確會暫時為你保留 i 的記憶體空間,不過因為在把函式推送到陣列裡面的時候,並沒有立即引用到 i ,所以等到 pushFuncToArray 結束,一個一個執行 functionArr 裡面的函式時, i 早就已經被 for 迴圈修改為 3 (因為迴圈已經結束,i 維持在跳出迴圈之前的值 ) ,這時候怎麼拿,當然 i 都會是 3 啦!

之後還會講到相同的概念,如果你有點不懂,後面還會再講到,但請盡量確保一定要弄清楚再往下囉! 那麼我們明天見。


上一篇
JS 原力覺醒 Day07 - 陳述式 表達式
下一篇
JS 原力覺醒 Day9 - 原始型別與物件型別
系列文
JavaScript 原力覺醒 - 成為絕地武士之路30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
sam0090175
iT邦新手 5 級 ‧ 2020-02-04 11:38:23

請問第一個例子
getEnemyInfo 函式原始碼 在哪裡?

Spark iT邦新手 5 級 ‧ 2020-02-04 14:03:34 檢舉

Sam 你好:
感謝回覆,應該是我當初上架時有漏掉一部分的範例程式碼,導致表達不清楚,剛才已經修改並整合進文章了,重新再跟你解釋一次,最新的範例在這邊:


function getEnemyInfo(){
	let enemies = ['Darth Vader','Sheev Palpatine'];
  let enemyLeader = 'Sheev Palpatine'
	 return enemies
}

function getBattleInfo(){
	let fellowInfo = ['Clone' , 'Clone' , 'Warship', 'Clone'];
	let enenyInfo = getEnemyInfo() 

	// enemyLeader is not defined 
  // because it's located in another function.
	console.log(enemyLeader) 

	return `the number of fellows is: ${fellowInfo.length }, and
					the number of enemies is" ${enenyInfo.length}` 
} 

getBattleInfo() 

以這個例子我想表達的是在一個函式內宣告的變數,是無法於外部另外一個函式的作用域取得的。

但是反過來卻可以,這時候 JS 會根據程式碼的實際位置(語彙環境)與目前執行到哪一個函式(執行環境)來決定閉包會不會產生。

如果還有不了解也可以再發問,我會盡可能回答你的。再次感謝你的發問!

謝謝你,你發布的文章很受用~

0
jim63
iT邦新手 5 級 ‧ 2020-04-12 22:12:43
function pushFuncToArray(){
		var funcArr = [] 
		for (var i=0; i<3; i++){
			 funcArr.push(function(){
					console.log(i)
				}) 
		} 
		return functionArr
} 

return functionArr 應該是 return funcArr XD

Spark iT邦新手 5 級 ‧ 2021-09-04 22:47:17 檢舉

jim63 謝謝你喔!不好意思筆誤,已經修正啦!
(你的眼睛也太尖了哈哈哈)

我要留言

立即登入留言