iT邦幫忙

2021 iThome 鐵人賽

DAY 21
2

前情提要

艾草:「來,接球!」

(我看著眼前一顆球飛了過來,正準備伸出手接時,突然變成了兩顆。)

「咦咦,為何?」

艾草:「我用魔法複製了一顆球呀!」

「這麼好用,那我能不能拿來複製房子呀?」

艾草:「沒那麼好的事,如果複製大型物件像房子,魔法只能複製出它的樣子,但你去改房子的擺設,也會連動影響到原本的房子,因為它們實際上還是存在同一個空間。」

「它的界線在哪呀?求教學。」

https://ithelp.ithome.com.tw/upload/images/20211005/20139066Bc9ts0BFhf.png

(不知道有沒有機會複製錢錢 ($ε $ ) )


傳值(by value)、傳參考(by reference)

在開始介紹傳值、傳參考前想先補充一個小知識。

其實我們每宣告一個變數,都會有相對應的記憶體空間存放變數,也會有另一個記憶體空間存放變數的值,例如你宣告一個變數 a 值為數字 1 的話,可以想像記憶體會如下表格:

https://ithelp.ithome.com.tw/upload/images/20211005/201390667sLIPaGKRq.png

了解了記憶體空間的小概念後,讓我們接著學習囉!

原始型別

在值為原始型別的情況下如果設變數定 ab 的值相同時,比較 ab 會得到以下結果:

let a = 1;
let b = 1;
console.log(a === b)//true

在原始型別的情況下,是去比較這兩個是否相等。

而當將 a 的值賦予給 b 時,其實它會直接拷貝 a 的值數字 1 過來,因為是複製的關係,所以今天將 b 重新賦予一個數字型別的 2 ,也不會影響到 a

let a = 1;
let b = a;
b = 2;
console.log(a)//1

在原始型別的情況下,因為是傳值的關係,所以就算我改變了變數 b 的值,也並不會連帶影響到變數 a ,他們的變數記憶體指向就是兩條平行線,如圖:

https://ithelp.ithome.com.tw/upload/images/20211005/20139066BSFpnns7c2.jpg


物件型別(包含函式)

而物件型別(包含函式)傳參考 (by reference)有什麼特性呢?

在針對物件去做比較時會發現以下情況:

let obj = {
	key:'value'
}
let newObj = {
	key:'value'
}
console.log(obj === newObj)//false

會發現裡面的屬性與值明明是一樣的,但回傳比對結果卻是 false ,因為物件是傳參考,所以比對的是記憶體位置,還不太了解沒關係,讓我們繼續看下去。

let obj = {
	key:'value',
};
let newObj = obj;
console.log(obj === newObj) //true
newObj.name = "王小明";
console.log(obj)//{key: "value", name: "王小明"}

像這樣明明我們改的是 newObj 卻連 obj 也更動了,原因就是因為物件是透過傳參考的形式,傳參考代表 newObj 其實是指向 obj 的記憶體空間,如圖:

https://ithelp.ithome.com.tw/upload/images/20211005/20139066JxDDqCLS0R.png

我們簡單替他們的記憶體空間編碼為 0x001,可以清楚地看到兩者都是指向同一個記憶體空間,所以當你改動 newObj 時也會同時改動到 obj

補充:在變數宣告章節有提到關於 const 宣告原始型別難以被重新賦予值,但物件型別因為是傳參考的特性,所以只要不改變記憶體空間就不會報錯,如下:

//不會報錯
const obj = {
	key:'value'
}
obj.name = "王小明";

//會報錯
const obj = {
	key:'value'
}
obj = {};//Uncaught TypeError: Assignment to constant variable.

只要不直接賦予該變數一個新的物件 {} 大括號,只是單純去修改物件值,不影響到記憶體空間的情況下,可以透過 const 去宣告物件型別的值。

小結:原始型別的值是透過拷貝的方式,所以並不會互相影響,物件型別會共用同一個記憶體空間,所以會互相連動。

而該如何避免這個情況呢?

我們可以透過以下幾種方法:

淺層拷貝

淺層拷貝指可以將第一層的位置的記憶體位置指向其他記憶體空間,但如果物件內又包了一個物件的值,該物件的值還是會與原物件共用一個記憶體空間:

Object.assign() 語法,可以複製一個或多個物件所有的屬性到新物件上,如下:

let obj = {
	key:'value'
}
let newObj = Object.assign({},obj);
console.log(obj === newObj)//false

展開運算子 ... ,透過 ES6 的新語法展開運算子也可以達成淺層拷貝:

let obj = {
	key:'value'
}
let newObj = {
...obj
};
console.log(obj === newObj)//false

深層拷貝

直接使用將物件轉字串又轉回物件的方式。

let obj = {
	key:'value'
}
let newObj = JSON.parse(JSON.stringify(obj));
console.log(obj === newObj)//false

總結

  • 原始型別為傳值
  • 物件型別(包含函式)為傳參考
  • 淺層複製 Object.assign() 、展開運算子
  • 深層複製 JSON.parse(JSON.stringify(obj))

小練習

請問以下敘述何者錯誤?
A 原始型別的情況下是傳值,物件型別的情況是傳參考
B 用 const 宣告的情況下,無論原始型別或物件型別,都不能修改任何值
C 如果想淺層拷貝可以透過展開運算子、Object.assign()語法
D 傳參考指的是記憶體會指向同一個記憶體位置

解答:用 const 宣告的物件型別,因為物件傳參考的特性,在不透過 = 重新賦予記憶體空間的情況下,進行屬性值的新增、修改並不會報錯。


參考文獻

0 陷阱!0 誤解!8 天重新認識 JavaScript!(iT邦幫忙鐵人賽系列書)
Vue 3 實戰影音課程(六角學院)


上一篇
中階魔法 - 範圍鍊 Scope Chain
下一篇
中階魔法 - 陳述式與表達式
系列文
JavaScript 魔法入門 - 從入門到中階觀念30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言