by reference (傳參考)、by value(傳值)的差別

在參加鐵人賽的時候,因為這個題目了解的朦朦朧朧、似懂非懂,所以不敢寫這個題目。直到JS直播班聽了老師的解說,才一秒突破盲腸,恍然大悟。

談這個題目之前,先來做一下六角學院JS直播班第一週的周末作業《記憶體接龍》,以了解變數與記憶體儲存位置的關係,這樣對於by reference (傳參考)、by value(傳值)會有更深入的了解。

第 1 題

// 1. console.log 的值為?
// 2.出現幾個變數、型別、記憶體空間?
let a ;
a = 1;
a = "hello";
console.log(a)

//答案1:console.log 的值為 hello
//答案2:1個變數a;3個型別(undefined、number、string);3個記憶體空間

記憶體空間畫圖來表示:

備註: let a ;的值為 “ undefined “ ,也會佔記憶體空間。
所以答案2應該是: 1 個變數, 3 個型別, 3 個記憶體空間

助教的回答:let a因為並沒有宣告a的值,所以a會有一個undefined 的值,並且佔了一個記憶體空間,所以這題答案應該是1個變數, 3 個型別(數字、字串、 undefined ), 3 個記憶體空間。

第 2 題

// 1. console.log 值為?
// 2.出現幾個變數、型別、記憶體空間?
let b = 3 ;
b=5;
let c = 4;
b=8;
c=c+b;
let d = b+c;
console.log(e)

//答案1:console.log 的值為「Uncaught ReferenceError: e is not defined」
//答案2:3個變數;1個型別(number);6個記憶體空間

記憶體空間畫圖來表示:

第 3 題

// 1. console.log 值為?
// 2. 出現幾個變數、型別、記憶體空間?
let e=0;
e = 5;
e = "hello"
e = true;
e = 3;
e+=1;
console.log(e)

//答案 1:console.log 的值為 4
//答案 2: 1 個變數; 3 個型別(number、string、boolean); 7 個記憶體空間

記憶體空間畫圖來表示:

我們由上面的作業可以發現,不同的變數指向不同的記憶體位置,只要變數重新賦值之後,就會把新的值存到新的記憶體空間之中,舊的值就從記憶體上面清空,而運算過程中的值也會佔到記憶體的空間。

這就是JS變數與記憶體之間運作的過程。

還有一個觀念要先記一下, JS 的變數本身沒有型別,它被賦予的值才有

理解了這些才能進一步來談 by reference (傳參考)、 by value (傳值)的差別。

by value(傳值)

讓我們繼續來《射鵰英雄歪傳》,郭靖跟黃蓉小倆口結婚後,在大漠開起了寵物店,專門賣汗血寶馬和神雕,因為都是珍稀之物,所以定價都是1000兩黃金。

// 設一個汗血寶馬(horsePrice)的變數,給它1000的值
let horsePrice = 1000;
//設一個神雕(eaglePrice)的變數,也給它1000的值
let eaglePrice = 1000;

console.log(horsePrice === eaglePrice);
//true

我們可以觀察到,在基本型別的時候,不同的變數指向不同的記憶體位置,兩個變數賦予的值一樣,也就是記憶體儲存的值一樣,兩個變數就相等。所以我們可以歸納出,基本型別變數的比較,我們看的是它被賦予的值,值相等,兩個變數就相等。

繼續來《射鵰英雄歪傳》,有一天楊康來寵物店想買一隻汗血馬,問郭靖多少錢?因為楊康之前買過神雕,郭靖隨口就說:「汗血馬跟神雕一樣的價格!」

楊康心想:「老子最近沒錢!」就說:「兄弟!這馬也太貴了!」郭靖說:「蓉妹說不二價,兄弟!聽某嘴大富貴!」於是楊康只好忍痛去跟大漠的高利貸歐陽克借錢買了一匹汗血馬。

結果過幾天,成吉斯汗打敗大宛國,擄獲許多汗血寶馬,造成大漠上汗血寶馬的價格大跌價,一匹馬變成300兩黃金。

我們就用 JavaScript 來說說這件事:


//設一個神雕(eaglePrice)的變數,也給它1000的值
let eaglePrice = 1000;
//郭靖跟楊康說:汗血馬和神雕的價格是一樣的
let horsePrice = eaglePrice;

console.log(eaglePrice); //1000
console.log(horsePrice); //1000
console.log(eaglePrice === horsePrice); //true

//結果成吉思汗擄獲許多汗血馬,造成汗血馬價格大崩壞
horsePrice = 300;
console.log(eaglePrice); //1000
console.log(horsePrice); //300
console.log(horsePrice === eaglePrice); //false
//true

楊康哭哭!汗血馬的價格不是跟神雕一樣嗎?

讓我們用前面畫圖的練習來理解一下JavaScript發生了甚麼事!

//設一個神雕(eaglePrice)的變數,也給它1000的值
let eaglePrice = 1000;
//郭靖跟楊康說:汗血馬和神雕的價格是一樣的
let horsePrice = eaglePrice;
console.log(eaglePrice === horsePrice); //true
  • 宣告一個eaglePrice變數,給它1000的值
  • 宣告一個horsePrice變數,給它的值是變數eaglePrice。

這時發生的事情就是,horsePrice 去拷貝了 eaglePrice 的值 1000 到自己目前占用的記憶體空間。前面有提到「基本型別變數的比較,我們看的是它被賦予的值,值相等,兩個變數就相等」。

let horsePrice = eaglePrice;
這時候eaglePrice === horsePrice的布林值為true。

//horsePrice重新賦值為300
horsePrice = 300;
console.log(horsePrice === eaglePrice); //false

horsePrice重新賦值為300這個行為指的是,horsePrice去佔用了新的記憶體空間儲存了新的值300,這時horsePrice === eaglePrice的布林值就是false。

汗血馬的價格horsePrice與神雕的價格eaglePrice是各自獨立的,當值相等時,兩個變數才相等,汗血馬價格崩盤的時候,神雕的價格依然不受影響。

所以我們可以說「基本型別」變數之間的比較,看的是它被賦予的值相不相等,這種現象被稱為「by value(傳值)」。

by reference (傳參考)

但是在「物件型別」的比較上,是另外一種情形。

繼續來《射鵰英雄歪傳》,郭靖的寵物店因為生意太好,所以開了分店,分店裡面汗血馬的價格跟總店是一樣的。

//mainStore物件儲存總店汗血馬的價格
let mainStore = {horsePrice: 1000};

let branchStore = mainStore;

console.log(mainStore.horsePrice);  //1000
console.log(branchStore.horsePrice);//1000

branchStore.horsePrice = 300;

console.log(mainStore.horsePrice);  //300
console.log(branchStore.horsePrice);//300

console.log(mainStore === branchStore); //true

我們發現當分店汗血馬的價格branchStore.horsePrice被重新賦值為300時,總店的汗血馬的價格也跟著變為300。

而console.log(mainStore === branchStore)的結果為true,我們可已發現mainStore與branchStore指向的是同一個實體。

我們來看看宣告物件型別變數時,記憶體是如何運作的。

當我們let mainStore = {horsePrice: 1000};其實是把mainStore的參考位置指向記憶體中存放物件的位置。

所以當我們let branchStore = mainStore;也是把branchStore變數參考的位置指向mainStore所參考的變數位置,所以當物件horsePrice屬性的值改變的時候,mainStore跟branchStore的值都會跟著變動。

物件型別是透過「引用」的方式在傳遞資料,物件型別的物件的屬性值其實引用的是記憶體儲存資料的參考, 所以我們會說在物件型別的比較是by reference(傳參考),看這兩個物件是否指向相同的記憶體空間,參考相同的值。

凡事都有例外,物件的例外讓人特別困惑。

繼續來《射鵰英雄歪傳》,郭靖寵物店生意很好,所以黃蓉也開了一家分店,有一天夫妻倆吵架,黃蓉一氣之下脫離加盟體系,開始削價競爭。

//husbandStore物件儲存郭靖寵物店汗血馬的價格
let husbandStore = {horsePrice: 1000};
//把husbandStore的值指定給wifeStor
let wifeStore = husbandStore;
//wifeStore物件重新賦值
wifeStore = {horsePrice: 500};

console.log(husbandStore.horsePrice);  //1000
console.log(wifeStore.horsePrice);//1000

wifeStore.horsePrice = 300;

console.log(husbandStore.horsePrice);  //1000
console.log(wifeStore.horsePrice);//300

console.log(husbandStore === wifeStore); //false

在這種情形之下,husbandStore與wifeStore原本是引用相同的參考位置,但是wifeStore重新賦值之後,則引用新的參考位置,所以汗血馬價格變動的時候,husbandStore與wifeStore兩者不會連動,因為兩者參考的是不同的物件實體。

許國政先生認為這種物件型別的比較應該更像是「by sharing」,這有點玄!

且讓我們引用他在《0 陷阱!0 誤解!8 天重新認識JavaScript!》的一段話作為總結:

「由於JavaScript的物件類型是可變的(mutable),當物件更新時,會影響到所有引用這個物件的變數與副本,修改時會變動到原本的參考。但當賦與新值時,卻會產生新的實體參考。」

參考資料


by reference (傳參考)、by value(傳值)的差別
https://popeye-ux.github.io/2021/10/15/byReferenceByValue/
作者
POPEYE
發布於
2021年10月15日
許可協議