iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 14
0
Modern Web

JS30 錄系列 第 14

Day 14 - Reference Copy or Value Copy ?

任務目標

能夠區別變數值在傳遞時是透過「複製參考」還是「複製值」。

重點整理

在撰寫JS程式碼時,常會有將值指定給變數一,再把變數一指定給變數二這種傳遞值的行為。但這種值的傳遞行為可能會隨著值的型別不同而有不同的結果。讓我們看下去。

Value Copy

當值的型別為純量的基值時,將以複製值(Value Copy)的方式完成值的傳遞。

舉例如下:

// 將值指定給變數一
let age = 100;
// 將變數一指定給變數二
let age2 = age;
// 變數一和變數二的值相同
console.log(age, age2);
// 把新的值指定給變數一
age = 200;
// 變數二依然故我
console.log(age, age2);

將 100 指定給變數 age,再將變數 age 指定給變數 age2 ,當重新指定一個新的值給 age 時, age2 不會因此而跟著改變。

這是因為在將 age 指定給 age2 的過程中,發生了複製值的行為。新的值 100 先被複製出來放到新的記憶體位置,然後再指定給 age2。由於變數 ageage2 分別存取著不同記憶體位置的 100 ,就算 age 被指定了任何新值,也與 age2 無關。

當變數存的值是純量的基值(Primitive)時,例如: NumberStringBoolean 等,值的傳遞就會以上述方式進行。

Reference Copy

當值的型別為較複雜的陣列或物件時,將以複製參考(Reference Copy)的方式完成值的傳遞。

舉例如下:

// 超新星樂團 - 吉木蛙鵝
let superTeam = ['Otree', 'Penguin', 'Frog', 'Jim'];
// 成立山寨樂團 - 吉姆挖耳
let fakeTeam = superTeam;
// 兩個變數值相同
console.log(fakeTeam, superTeam)
// 山寨樂團團員財務糾紛 -> 緊急人事異動 -> <吉姆挖坑> 重新出道
fakeTeam[3] = 'Chris';
// 超新星樂團竟然也跟著變了!?
console.log( fakeTeam, superTeam);

將陣列指定給變數 superTeam ,再將變數 superTeam 指定給變數 fakeTeam ,當 fakeTeam 的值被更改時,原來的 superTeam 竟然也跟著被更動了,究竟是為什麼呢?

這是因為 superTeam 擁有的是陣列這種複合值,在這種情況下,一個指向存放 ['Otree', 'Penguin', 'Frog', 'Jim'] 這個值的記憶體「路徑位置」會被複製,然後路徑位置會被指定給新的變數 fakeTeam 。 因此兩者都是參考到相同記憶體內的陣列。造成了更改新變數的值,舊變數也跟著被更動的結果。

當變數為複合值時,如 FunctionArrayObject 時,將以上述方式完成傳遞。

Call by Sharing (共享參考)

上述第一種傳遞值的方式,稱為 Call by Value ,其定義為當一個變數值被呼叫的時候,永遠都會先複製一份該值,然後再傳給呼叫它的對象。

而第二種傳遞值的方式,又稱作 Call by Reference,其定義為當一個變數值被呼叫時,會直接把該變數值所在的記憶體位置當作參考,直接扔給呼叫它的對象,因此不管新的對象更改該值或是甚至賦予新值,都會改變原來的變數值。

問題來了,看看下面的程式碼:

// 超新星樂團 - 吉木蛙鵝
let superTeam = ['Otree', 'Penguin', 'Frog', 'Jim'];
// 成立山寨樂團 - 吉姆挖耳
let fakeTeam = superTeam;
// 兩個變數值相同
console.log(fakeTeam, superTeam)
// 用重新指定值的方式對山寨樂團人事異動
fakeTeam = ['Otree', 'Penguin', 'Frog', 'Chris'];
// 超新星樂團沒有跟著變!
console.log(fakeTeam, superTeam)

剛有提到變數值傳遞若為 Call by Reference ,即使是賦予新值,原值也會跟著改變,但在 JS 出現了原值聞風不動的例外! 究竟是為什麼呢?

有些人認為這是由於 JS 根本沒有所謂的兩種傳值方式。會產生這種看起來像是有兩種傳值方式的假象是由於一種叫做 Call by Sharing 的變數傳值方式所造成的, JS 從頭到尾都只有一種傳值方式,其名為 Call by Sharing 。定義如下:

「變數值被呼叫時,會直接把該變數所在的記憶體位置做為參考,傳遞給呼叫它的對象。但當新對象被賦予新值時,新對象會被指定一個新的記憶體位置。」

因此當修改值時,舊的變數值也會跟著被修改,但當新變數被賦予新值後,新舊變數兩者參照到不同的記憶體位置,從此再無瓜葛。

這麼一來解釋了上面的程式碼,但為何會產生基值和複合值之間擁有不同傳遞方式的錯覺呢?

因為在 Javascript 中,所有的基型值的型別皆為不可變動的(Immutable),永遠只能藉由賦予新值的方式將值傳遞給新的變數,差別在此而已。

常常我們需要將資料從舊的陣列複製到新的陣列做處理,然後不想影響到舊的陣列。依照這個原理,是不是就無法實現了?難道,這就是複合值的宿命了嗎?

別怕,既然陣列指定到新變數時不會自動複製,我們在指定前先手動複製不就得了嗎?

以下方式可以達成想要的目標:

// 用 slice() 方法複製新陣列再指定給新變數
fakeTeam2 = superTeam.slice();
// 用 Array.from() 複製新陣列再指定給變數
fakeTeam3 = Array.from(superTeam.slice());

總之想達成目標,核心觀念就是先行手動複製,然後再指定。

以上就是 JS30 第十四篇!

Reference

Call by Sharing - 在第1273則回應
Call by Sharing - 一個好簡報
Primitive 的型別是不可變動的


上一篇
Day 13 - Slide in on Scroll
下一篇
Day 15 - Local Storage
系列文
JS30 錄30

尚未有邦友留言

立即登入留言