[JavaScript] Javascript中的傳值 by value 與傳址 by reference
先看兩個有趣的例子:
var a = 'Hi'; // 定義 a 的內容為'Hi'字串
var b;
b = a; // 將 b 指定等於 a 字串
console.log(b); // 'Hi'b = 'Hello'; // 再將 b 的內容更動為'Hello'
console.log(a); // 'Hi'
console.log(b); // 打印 b 出現變更後的內容'Hello'
看起來就跟想像中差不多,再來看下一個:
var aa = {age:18}// 定義 a 是內容為{age:18}的物件
var bb;
bb = aa; // 將 b 指定等於 a 物件
console.log(bb); // {age:18}bb.age = 20; // 再將 b 的age值更動為 20
console.log(aa); // {age:20} !?!?!?
console.log(bb); // {age:20}
😲😲😲 !?!?
我改變了 bb,aa 也跟著改變了,可是剛剛不會阿 !?
在 Javascript 中,參數的傳遞分為「傳值」和「傳址」兩種方法:
- 傳值可稱為 Call by value 或 Pass by value
- 傳址則稱為 Call by reference 或 Pass by reference,或是有時候也會聽到的「傳參考」
基本型別處理值 by value
Javascript 中除了物件以外的所有型別,都是基本型別,物件的定義是一對 key + value
,而基本型別,就只是一個純值,稱為基本型別 (Primitive type),在 Javascript 會是以傳值 by value 的方式傳遞,基本型別共有以下六種:
- Boolean:布林
- Null:空值
- Undefined:未定義
- Number:數字
- String:字串
- Symbol(於 ECMAScript 6 新定義):符號
變數、記憶體位置、值
在電腦底層的世界,可以想像記憶體空間就像是一個一個空間,每一個空間都有他的位址,並可以空間內儲存值。為了方便人類取用,才有了「變數」的存在,拿來連結(指向)這些記憶體位址,宣告變數賦值,就是向電腦要一個記憶體空間來存值。
以平常的這個動作來說:var a = 'emma'
,其實是變數 a 指向電腦中某記憶體的位置(ex: 0x01) ,在這個記憶體位置中,儲存 emma 這個值。
如果我再宣告了一個 b:var b = a;
,雖然 b 和 a 的值一樣都是 'emma'
,但其實變數 b 是指向了另一個不一樣的記憶體位置 (ex:0x02),把 a 的值 copy 過來存,a 和 b 是存在於兩個獨立不同的記憶體位置中:
並基本型別有兩大特色:
- 只是一個值,不會有屬性
- 不可變異 (immutable)
怎麼說不可變異呢?如果我們改變了 a 的值,其實是改變 a 所指向的記憶體位置,'emma'
這個值是永遠不會被改變的,只是 a 所指向的記憶體內容不同了,這就是所謂的不可變異。
物件型別處理參考 by reference
除了基本型別,其他型別都算是物件型別,則會以 by reference 傳址的方式傳遞:
- Object:物件
- Array:陣列
- Function:函式
假設我建立了一個陣列:var arr1 = [1,2,3]
,表示 arr1 指向了一個新的記憶體位置(ex: 0x01),而如果我再建立了第二個陣列並讓他等於 arr1: var arr2 = arr1
,這時候 arr2 則會直接指向 arr1 的記憶體位置(0x01),所以不論 arr1 所儲存的值是多少, arr2 都會得到一樣的值:
ex:
var arr1 = [1,2,3];
var arr2 = arr1;
console.log(arr2); // [1,2,3]arr1[0] = 2;
console.log(arr1); // [2,2,3]
console.log(arr2); // [2,2,3]
但是有一個很重要的例外,就是如果是直接以等號賦予新的值,就會再建立一個新的記憶體位置,在下面的範例中,arr1 和 arr2 將不再指向同一記憶體位置:
var arr1 = [1,2,3];
var arr2 = arr1;
console.log(arr2); // [1,2,3]arr1 = [4,5,6]; // 賦予新值
console.log(arr2); // [1,2,3]
若是以修改的方式則不算在此例外狀況內:
var obj1 = {name: 'emma'};
var obj2 = obj1;
console.log(obj2); // { name:'emma' }obj1.age = 18; // 修改 obj1
console.log(obj1); // { name:'emma', age:18 }
console.log(obj2); // { name:'emma', age:18 }
Pass by sharing
在 Javascript 中的傳值和傳址之間其實有幾個萬年的爭議點:
- Javascript 只有 Pass by value
- Javascript 其實不是 Pass by value/reference,而是 Pass by sharing
先說結論:
不管 Javascript 是 Pass by value/reference/sharing,重點都應該在理解這幾種方式行為上的差異,而不是在名詞的定義上。每個人心中的定義不同,這是一個沒有標準答案的問題,如果你想要知道大家在吵什麼,可以再繼續往下看。
那 Pass by sharing 又是什麼?
剛剛在說明 pass by reference 的例外狀況時,就是爭議點中所認為,pass by sharing 可以完美解釋 Javascript 資料傳遞的地方。
以 pass by reference 的定義來說,就算我用等號直接定義了新的內容,引用到他的變數也應該要一起變動,但是我使用了等號和物件實字 (Object literal) 或是陣列實字 (array literal) 的方式改變值,卻讓這個變數指向了新的記憶體位置。
這個用大括弧直接賦予或定義物件值的方式,稱為物件實字 (Object literal),
ex: var emma = {age:18}同理用中括弧直接賦予或定義陣列值的方式,稱為陣列實字 (Array literal)。
ex: var fruits = []
pass by sharing 的定義就有點像是融合了call by value 和 call by reference:
- 碰到基本型別,表現行為是 Pass by value。
- 碰到物件型別,如果只是改變內容,表現行為是 Pass by reference,但是如果對物件作重新賦值(literal),表現行為是 Pass by value。
所以才會有這樣的論點,覺得 Javascript 其實是 call by sharing。
那 Javascript 只有 call by value 又是為什麼?
讓我們再看一次剛剛 call by reference 的示意圖:
其實以物件型別來說,變數儲存的都是記憶體位址,拿來找出位址中儲存的值,更底層的示意圖會展開像是這樣:
所以在 arr2 = arr1 的這個動作中,都是把 arr1 中儲存的值,複製一份丟進 arr2 中,不論複製的內容是「值」或是「位址」。
這就是為什麼會有人認為,Javascript 一律都是 Pass by Value。在別的程式語言中,有的語言可以在傳遞資料的時候選擇要以 by value 或是 by reference 的方式傳遞,但是在 Javascript 中,是沒有辦法自己選擇的。
內容若有任何錯誤,歡迎留言交流指教! 🐬
ref:
你不可不知的 JavaScript 二三事#Day26:程式界的哈姆雷特 — — Pass by value, or Pass by reference?
[筆記] 談談 JavaScript 中 by reference 和 by value 的重要觀念
JavaScript 的「傳值」與「傳址」
JS基礎:Primitive type v.s Object types
[JavaScript Weird]Day 27 觀念小叮嚀:傳值和傳參考
深入探討 JavaScript 中的參數傳遞:call by value 還是 reference?
重新認識 JavaScript: Day 05 JavaScript 是「傳值」或「傳址」?
簡單介紹JavaScript參數傳遞
JavaScript — 參數傳遞方式 (2)