# [JavaScript] pass by value(傳值)、pass by reference(傳址)還是 pass by sharing ###### tags: `前端筆記` ## 總結 - ==變數不是直接保存值,而是指向保存值的記憶體(限 Call Stack 的區域)== - value 的定義會影響是否有 pass by value, pass by reference or pass by sharing 還是一律只有 pass by value - ==核心觀念:如果是基本型別,就是不可改變的,會新增記憶體;如果是物件型別,就是可改變的,除非新建立一個物件(物件實字 Obejct Literal 或者陣列實字 Array Literal),就不會新增記憶體。== ## 事前須知 談到這三個觀念之前必須先探討 JavaScript 的資料型別,因為資料型別會決定一個變數是用什麼方式傳遞的。JavaScript 的資料可以分成「基本型別」以及「物件型別」兩大類。因為資料型態的不同也會影響變數儲存值的方式,所以還要理解 JavaScript 是如何「記憶」變數的。 ### 1.資料型別的探討 #### 1. 基本型別(Primitive data type) 1. 字串(String) 2. 數字(Number) 3. 布林值(Boolean) 4. undefined 5. null => 即使 `typeof` 出來的是 `object` 但我還是屬於基本型別的喔 ![](https://i.imgur.com/wgFJmbT.png) #### 2. 物件型別(Object type) 除了基本型別之外都是物件型別 - 即使陣列及物件都有自己的方法可以用,但在 JavaScript 的眼中陣列就是物件 - 即使函式 `typeof` 得到的是 *function* 但在 JavaScript 眼中函式就是物件 ![](https://i.imgur.com/7zqB674.png) 我是等到需要「認真地判別」才會覺得陣列跟函示是物件,平常寫 code 就把他們想成是物件裡面個別的東西。 ### JavaScript 是如何「記憶變數的」? **不同的資料型態,值保存的位置會不一樣** ```javascript= let myNum = 12; ``` 一開始我認為的方式: 1. JavaScript 會建立一個記憶體(Memory,比方來說 0x001) 2. 0x001 會保存值(12) 3. JavaScript 再創造一個變數名稱(也就是 `myNum`) 4. `myNum` 會保存值(12) 結果仔細讀完這篇厲害的文章:[JavaScript’s Memory Model](https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239) 後,發現事情不是那麼簡單... 其實是另一個方式: 1. 建立一個 identifier (名稱就是宣告的變數名稱,也就是 `myNum`) 1. 會先判別值是什麼型別(基本型別 Primitive data type 或者物件型別 Object type) 2. 發現是基本型別 3. 在 Call Stack 建立一個記憶體(Memory,比方來說 0x001) 4. 0x001 保存值(12) 5. 將 `myNum` 指向「記憶體」0x001,讓人類可以藉由 `myNum` 找到記憶體 0x001 裡面的值 所以正確來說:`myNum` 是指向「記憶體」,不是直接指向「值」;當看到 `let myNum = 12;` 其實代表的是:==`myNum` 是指向一個保存的值是 12 的記憶體,而不是指向值。== 接下來再看這個例子 ```javascript= let myNum = 12; let myNum2 = myNum; ``` ![](https://i.imgur.com/HdjytVR.png) 依照「變數名稱是指向保存值的記憶體」的原則來看,實際的樣子應該是: 1. 建立一個 identifier (名稱就是宣告的變數名稱,也就是 `myNum`) 1. 會先判別值是什麼型別(基本型別 Primitive data type 或者物件型別 Object type) 2. 發現是基本型別 3. 在 Call Stack 建立一個記憶體(Memory,比方來說 0x001) 4. 0x001 保存值(12) 5. 將 `myNum` 指向「記憶體」0x001,讓人類可以藉由 `myNum` 找到記憶體 0x001 裡面的值 6. 建立一個 identifier (也就是 `myNum2`) 7. `myNum2` 指向的記憶體與 `myNum` 相同,兩個記憶體都保存相同的值 ==這時 `myNum` 跟 `myNum2` 指向的記憶體(Memory)都相同(0x001),所以就有相同的值。== 如果現在是這樣子的話呢? ```javascript= let myNum = 12; let myNum2 = myNum; myNum2 += 1; ``` 現在就要理解另一個新的觀念了: >All primitives are immutable, i.e., they cannot be altered. It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. [ref. - MDN](https://developer.mozilla.org/en-US/docs/Glossary/Primitive) >所有基本型別值是不可改變的,這個觀念對於理解一個保存基本型態值的變數非常重要。該變數也許會被重新賦值,但是原本的值是不會像物件型別一樣,是絕對不會被更改的。 ==重點即是:所有基本型別的值是不會被改變的。== 套在這個例子上就是: 1. 建立一個 identifier (名稱就是宣告的變數名稱,也就是 `myNum`) 1. 會先判別值是什麼型別(基本型別 Primitive data type 或者物件型別 Object type) 2. 發現是基本型別 3. 在 Call Stack 建立一個記憶體(Memory,比方來說 0x001) 4. 0x001 保存值(12) 5. 將 `myNum` 指向「記憶體」0x001,讓人類可以藉由 `myNum` 找到記憶體 0x001 裡面的值 6. 建立一個 identifier (也就是 `myNum2`) 7. `myNum2` 指向的記憶體與 `myNum` 相同,兩個記憶體都保存相同的值 8. ==建立一個新的記憶體(0x002)保存值 1== 10. ==建立新的記憶體(0x003)保存 12 + 1 的結果(13)== 11. ==`myNum2` 指向新的記憶體(0x003)== ![](https://i.imgur.com/NCt1Sov.png) 為什麼不在原本的記憶體(0x001)內更改值呢? **因為基本型別的值是不可改變的**,所以該記憶體的值就永遠是 12,如果要更新值,那麼 JavaScript 會重新開一個記憶體保存新的值。 那如果是物件型態呢? ```javascript= let myArr = []; ``` 接下來就解鎖新的觀念了,就是「Call Stack」及「Heap」。 #### Call Stack 跟 Heap 是啥米? **Call Stack 保存基本型別** ```javascript= let myNum = 12; ``` 上面的例子畫成圖是這樣子: ![](https://i.imgur.com/RNcG5sZ.png) 流程都和上面相同,只是顯示出保存的位置,也就是「Call Stack」。 **Heap 則是保存物件型別** 回到例子 ```javascript= let myArr = []; ``` 此例畫成圖則是: ![](https://i.imgur.com/NINDTpC.png) 1. 建立一個 identifier (名稱就是宣告的變數名稱,也就是 `myArr`) 2. 接下來會判斷值是什麼型別(基本型別 或 物件型別) 3. 發現是物件型別! 4. 在 Call Stack 建立一個新的記憶體(比方來說:0x001) 5. 在 Heap 建立一個新的記憶體(比方來說:heapx001) 6. heapx001 保存物件型態的值([]) 7. Call Stack 的 0x001 保存 Heap 的記憶體(heapx001) 8. `myArr` 指向記憶體(0x001) **所以 `myArr` 是指向一個以 Heap 記憶體(heapx001)為值的 Call Stack 記憶體(0x001)。** ==而且與基本型態不同,物件型態是可以被改變的,所以當我們改變物件型態的值時,JavaScript 並不會為此新增一個記憶體。== ```javascript= let myArr = []; myArr.push(1); myArr.push(2); myArr.push(3); ``` ![](https://i.imgur.com/04uXlM1.png) ## 基本型別(Primitive data type) ### 1. 依照「值」判別 當值是「數字」時。 ![](https://i.imgur.com/id4ctSb.png) ![](https://i.imgur.com/V4WOpog.png) 為了防止記憶體大爆炸,當資料型別是基本型別時,JavaScript 會自動尋找有沒有相同的值,如果有的話就不會多開記憶體了。 **不要搞混是比較值(Call Stack),而不是比較記憶體!** 當值是「布林值」時。 ![](https://i.imgur.com/JL1XmBp.png) ![](https://i.imgur.com/FcGUgxd.png) 剩下「字串」、「undefined」以及「null」也是。 ![](https://i.imgur.com/Krh72l6.png) | identifier | address | value | |:---------:|:--------:|:------| | | 0x01 | undefined | | | 0x02 | null | | | 0x03 | 'Hello World' | ### 2. 更新及傳遞不會改變原來的值 ![](https://i.imgur.com/pfPWPVd.png) ![](https://i.imgur.com/GqZnAfW.png) 因為基本型別的值不可以被改變,所以 JavaScript 會重新建立新的記憶體保存新的值(20),`x` 也會重新指向新的記憶體。 ## 物件型別(Object type) ![](https://i.imgur.com/CxQS1F8.png) ![](https://i.imgur.com/OcIjO1u.png) 1. 建立 identifiter (也就是變數名稱 objA, objB) 2. 在 Heap 建立記憶體保存值({x: 10}) 3. 在 Call Stack 建立記憶體並保存 Heap 中的記憶體(也就是 Address) 4. `objA` 及 `objB` 指向對應的 Call Stack 記憶體 即便兩個物件的屬性(property = key + value) 都相同,但變數係保存物件的「址參器」並不是「物件本身」。兩個物件就會有兩個址參器,所以不相等。 **因為 `objA` 保存的 reference != `objB` 保存的 reference。** ![](https://i.imgur.com/j8pPMdv.png) 一開始先宣告 `objB` 指向與 `objA` 相同的記憶體,因為物件型態可以被更改(Objects are mutable.)所以不會開新的記憶體保存更改的值,而是在原本的記憶體直接改值。 ![](https://i.imgur.com/12mjpyc.png) ### (.)Dot Notation 跟 物件實字(Object Literal)大不同 **如果使用物件實字({})或者陣列實字([])賦值會導致新增一個新的物件 + 新增新的址參器。** ![](https://i.imgur.com/Dka0kCX.png) ![](https://i.imgur.com/ANs4DjI.png) 因為使用物件實字 = 創建一個新的物件,JavaScript 就會新增一個記憶體保存新的物件,此時 `objA` 就會指向新的記憶體(0x002),0x002 的值 也會時新的 Heap 的記憶體 heapx002。 同理,`function swap` 中是「物件實字」,是重新給新的物件(所以保存的就是新的址參器),但是函式中的 `obj` 變數只存活在函式中,脫離函式就掛了,所以在外層 `console.log(obj)` 就會報錯。 但是在 `function swap2` 中是 (.)Dot Notation,代表更新物件中屬性的值(value),所以函式中的 `obj` 指向的址參器和 `obj2` 是同一個。 ![](https://i.imgur.com/JevfZou.png) ![](https://i.imgur.com/AoI9F92.png) JavaScript 係按照值(也就是 Call Stack 的 value)來傳遞引數,當叫用一個函式,就代表函式會複製參數的值給引數。如果值是基本型別,如果在函式中更改值,並不會更改到函式外層的值。如果是物件型別,則有可能會更改,因為物件型別可以被更改的,Call Stack 中的 value 只是保存 Heap 中的 Address 而已。 「要注意的是引數脫離函式就掛了」所以函式外面撈不到 `obj`。 ![](https://i.imgur.com/BWnRXXo.png) ![](https://i.imgur.com/tD7JxoH.png) 所以得到這樣子的結果也不意外了。 A: 一開始 `arr2` 確實和 `arr1` 一樣指向同個記憶體,但是後來 `arr1` 變指向和 `arr3` 相同的記憶體了。 ## 番外篇,電腦看待變數與人類看待變數 **宣告變數就是告訴電腦:大大請把「位址」和這個代名詞連結吧** >對電腦來說,實際運作和辨認資料位置是以「位址」為準;變數名稱只是為了方便人類呼叫、類似別名的存在,由程式幫忙和實際的資料儲存位址作連結。 ->ref:[你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 —— Pass by value, or Pass by reference?](https://ithelp.ithome.com.tw/articles/10209104) 因為 JavaScript 可以分成「基本型別」及「物件型別」,變數保存的東西不同,圖像畫可以變成: ![](https://i.imgur.com/8R3OCve.png) - 電腦:變數是一個保存資料的位址(地址),如何保存資料要看「資料型別」是什麼 - 人腦:變數是一個代名詞,這個代名詞指向儲存資料的「位址」。因為人類宣告變數(也就是告訴電腦位址要和這個代名詞連結),所以人類可以藉由代名詞讀取「位址」中的值。 ## 小記 那麼一定要非得釐清是 pass by value, pass by reference or pass by sharing 嗎?在我看 [JavaScript’s Memory Model](https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239) 以前覺得是重要的,但是現在我認為理解 [JavaScript’s Memory Model](https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239) 中講解的 JavaScript Memory Model 才是重要的,因為這個觀念把 pass by value, pass by reference or pass by sharing 背後的原因講的更透徹,照這個原理,JavaScript 只有 pass by value,因為都是靠記憶體來存值的。不過就如同很多大神分享的,知道基本型態是不可以改變的,而物件型態是可以改變的(然後知道用物件實字及陣列實字是新增一個全新的物件),以及 JavaScript Memory Model,就可以了。 ## 參考資料 1. [深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?](https://blog.techbridge.cc/2018/06/23/javascript-call-by-value-or-reference/) => 內有為什麼 JavaScript 係只傳值的介紹 2. [你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 —— Pass by value, or Pass by reference?](https://ithelp.ithome.com.tw/articles/10209104) => 有額外補充變數的介紹 3. [重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?](https://ithelp.ithome.com.tw/articles/10191057) => 基本型態及物件型態的更新、傳遞介紹 4. [Day.19 「認識 JavaScript 記憶體堆疊、傳值 與 傳址」 —— JavaScript 物件 與 記憶體](https://ithelp.ithome.com.tw/articles/10273943?sc=iThomeR) 5. [JavaScript’s Memory Model](https://medium.com/@ethannam/javascripts-memory-model-7c972cd2c239) =>特級大神的文 6. [Immutability in JavaScript](https://www.youtube.com/watch?v=rHt8O9oGOrk)