前言:最近算是自學到一個階段~已經開始面試。這次參加鐵人賽的主題以 JS 基礎知識為主,並會盡量將面試碰到的問題稍做整理結合進內容。作者仍努力學習當中,若有錯誤還請指正~感謝大家!(•ө•)♡
開市第一篇來個 by value、by reference 小boss,自己對於這個概念看了不下十次了...沒慧根就只能勤能補拙啦!
截至目前的 JS 版本,JS 的資料型別依然分成兩大類:「原始型別、物件型別」,且不支援開發者自定義其他型別。
這篇主要內容是針對兩大類的介紹以及他們如何存值、取值。
原始型別包含以下七種資料類型
當我們要從這些型別引用值時,這些值都是屬於原始值 (primitive values)。
原始值是什麼呢?原始值代表不可被改變的值,聽起來有點抽象對吧~
舉個例子
var a = 'hey'
a.toUpperCase()
console.log(a) // 依然是 hey
a = a.toUpperCase()
console.log(a) // 得到 HEY
這邊我們先把 a 變量賦值為字串 hey,並且用了轉為大寫的 method,但可以發現沒有作用。因為字串 hey 是原始值,我們日常規範的英文 h 就是 h,h 不會是 y,也不是 H。又或者 1 就是 1,1 不會是 0,這就是原始值的意思。我覺得如果把原始值翻作原語也許會更好理解些~
而在 JS 中,原始值是無法改變的,導致第二行 a.toUpperCase() 沒有效果。
那麼為什麼 a = a.toUpperCase() 卻可以操作?要注意避免把賦值這件事跟原始值本身搞混了。
我們可以給變數重新分配一個「新值」,但不能去修改「原本的值」。
這個點是我後來才理解的,沒注意到就會忽略掉 ,主要是對原始值這個詞花了點時間才能意會。(´・_・`)
除了上述提到的原始型別以外,其他的值都是物件型別,而物件包括了 Function、Array、Object、Date、Map、Set 等等,總之若不是原始型別,就會歸到物件型別。
物件型別要詳細說的話,還得開更多篇幅才能好好解釋其中的奧妙( 菜雞如我也還在宇宙中探索 )。但以剛開始認識 JS 來說,我認為先記得物件型別跟原始型別的差異在於,物件型別是可變的、以及把這些類型分清楚就好,因為會關係到下半部分提到的另一個新手大魔王 by value、 by reference。
請記住,所有值最終都會是這八種資料類型其中之一
值屬於原始型別或是物件型別,影響到值被儲存、取用的方式。
先記得結論:
原始型別的值是 pass by value
物件型別的值是 pass by reference
在講兩者的區別前,大家必須先知道 JS 背後對於變數的值是怎麼儲存的。
瀏覽器運作時,會佔用電腦的內存來執行網站所需要的程式碼。它將內存分配給 HTML CSS JS 等檔案,而在 JS 中,做法是會把用來存變數值的地方分成 stack(棧) 跟 heap(堆)。
stack 佔用的空間比較小、系統分配效率高,因此所存的是 value 的值以及 reference 的指針,可以看做一種簡單儲存,每個值的大小都差不多。
heap 佔用的空間大、自己存完還得分配指針到 stack → 效率相對低,用來存 reference 的值。
( stack 跟 heap 其實底層原理還得探討到資料結構,但這就偏題了,所以本篇不論述太多 )
好的~了解上面之後就先來看 pass by value
原始型別的值是以 pass by value 的形式操作,儲存以及複製的都是「值本身」
當我們的變數是原始型別時,複製這個變數,會發生什麼事?
var idol1 = 'Taeyeon'; // 字串=原始值
var idol2 = idol1;
第一行 宣告 idol1 為一個變數, idol1 在 stack 會指向 'Taeyeon' 這個值
接著宣告 idol2 = idol1 ,此時 pass by value 的做法會在 stack 複製一個新的 'Taeyeon' 並指向 idol2
這種做法的坑在哪?我們現在把本來排定要第一場表演的 Taeyeon 改為 Joy
var idol1 = 'Taeyeon';
var idol2 = idol1;
idol1 = 'Joy'; // 更改 idol1 的值
console.log(idol2);
改變 idol1 的值後,你可能會想說 idol2 是從 idol1 複製來的,那 idol2 應該也是 Joy 對吧?
No~~~ 正如剛所說的,idol2 在 stack 其實是一個獨立的新值,所以 idol2 仍然是 Taeyeon !並不會被 idol1 影響。
這就是 pass by value 的特性,當你的值為原始型別時,彼此形同陌路(誤。
物件型別則是 pass by reference 引用數據類型
當我們創建一個變數並賦予值為物件型別時,他會在 stack 中存著一個指針,指針指向 heap 的值。此後,當 JS 解析你要取得該物件的時候,就先在 stack 檢索該地址,然後從 heap 取出實值。這麼解釋可能有點懞,看例子!
var obj1 = { name:'Agnes' }
var obj2 = ['apple','grape','lemon']
我們宣告了兩個物件,實際上是如此存儲的
obj1 在 stack 裡存的是一個檢索地址 0x001 ,這個 0x001 可以想成指針指向 heap 裡的值
那複製引用類型的值會發生什麼事?
var obj1 = { name:'Agnes' }
var obj2 = ['apple','grape','lemon']
var obj3 = obj1; // 把 obj3 賦值
這個動作會把 obj3 在 stack 的指針,指向跟 obj1 一樣的地址
特性是在我們改變值時,會連帶影響到其他指向同地址的變數
var obj1 = ['apple','grape','lemon']
var obj2 = { name:'Agnes' }
var obj3 = obj1
obj3[0]='banana'
console.log(obj1)
去實際看會發現 obj1 也被改成 banana 了,這就是取用同個地址的值的結果
所以要記得,如果是引用類型的數據~會影響彼此喔!
當了解這個觀念後,也許你會突然豁然開朗最初寫 code 的時候,為什麼有時候資料被改變,有時候卻沒有變?
再補充一個 pass by reference 的相關知識
我們剛所複製的方式是賦予另個變數,但如果是賦予完全同樣的值,那是會創建一個新地址喔!JS 並不會去比對「他們的值一樣,所以他們是同個物件。」
因此我們若更改 obj2 的值也與 obj4 無關
var obj1 = ['apple','grape','lemon']
var obj2 = { name:'Agnes' }
var obj3 = obj1
obj3[0]='banana'
var obj4 = { name:'Agnes' }
obj2.name = 'Bill' // 更改 obj2
console.log(obj4) // 依然是 Agnes
這時候頭腦動很快的小朋友就會開始想說,如果我想要擁有一樣的物件類型的值,但不要變動到原本的值該怎麼做?請 Google「深拷貝、淺拷貝」來處理!因為這觀念是個不小議題,這邊就不詳細說明,看之後有沒有篇幅再來筆記~
參考資料
https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Data_structures
https://www.javascripttutorial.net/javascript-primitive-vs-reference-values/
https://roy-code.medium.com/普通類型和對象的區別-棧內存-stack-堆內存-heap-44295724848c
https://www.cnblogs.com/heioray/p/9487093.html