基本型別及物件型別
JavaScript上所有的東西可以分成兩大類:純值 Primitive Types
和 物件型別 Object Type
基本型別aka純值 (Primitive Types)
數字 number
字串 string
布林值 boolean
undefined
null
符號 symbol
(ES6)
物件型別 (Object Type)
JavaScript 中除了 Primitive Types 以外的東西,全都是物件型別!
傳值 (by value)
基本型別為傳值特性,將純值 b 指向純值 a,當中 a 或 b 進行修改時,不會互相影響。
1 | var a = 1; |
傳參考 (by reference)
物件型別特性是傳參考,將兩個物件互相指向,修改任一物件內部的屬性,都會影響另一個物件,因為這兩個變數都被指向同一個記憶體位置。
物件
1
2
3
4
5
6var obj1 = { name: 'dylan', age: 18 };
var obj2 = obj1;
obj2.age = 21;
console.log(obj1); // {name: "dylan", age: 21}
console.log(obj2); // {name: "dylan", age: 21}陣列
1
2
3
4
5
6var arr1 = [1, 2, 3];
var arr2 = arr1;
arr2[0] = 10;
console.log(arr1) // [10, 2, 3]
console.log(arr2) // [10, 2, 3]
如果要避免傳參考,最簡單的方法可以這麼做。由於物件的指向都是傳參考,而純值為傳值,我們可以一一的去指向物件內的純值來避免這個問題,缺點是非常麻煩。
1 | var obj1 = { name: 'dylan', age: 18 }; |
淺拷貝及深拷貝的定義
來源: Stack Overflow
淺拷貝 | 深拷貝 |
---|---|
只是複製 collection structure,而不是 element 。當在指向第二層物件 時會出現傳參考問題 |
整個複製,包含 element 。所以當我們在使用多層物件時,要盡量用 deep copy |
淺拷貝 (Shallow Copy)
以下舉幾個常見的淺拷貝範例
Array.concat & Array.slice
使用這兩個的結果是一樣的,也是傳統較常見的淺拷貝方式。詳細的用法請參考這篇 JavaScript 陣列處理常用方法
Array.concat
1
2var arr1 = [1, 2, 3, {aa: 'aa'}];
var arr2 = arr1.concat();Array.slice
1
2var arr1 = [1, 2, 3, {aa: 'aa'}];
var arr2 = arr1.slice();
看看結果
1 | console.log(arr1); // [1, 2, 3, {aa: 'aa'}] |
試著改變陣列第一層的值,結果並沒有造成傳參考,證明這個拷貝是成功了
1 | arr1[0] = 'Dylan'; |
接著改變陣列中第二層物件內的值,結果發生了傳參考的問題
1 | arr2[3].aa = 'bb'; |
1 | console.log(arr1); // ["Dylan", 2, 3, {aa: 'bb'}] |
Array.from & Spread
這是兩個都是 ES6 新增的,效果和 slice
及 concat
是一樣的,但是可讀性較佳,算是一種語法糖。
Array.from
1
2var arr1 = [1, 2, 3, {aa: 'aa'}];
var arr2 = Array.from(arr1);Spread
1
2var arr1 = [1, 2, 3, {aa: 'aa'}];
var arr2 = [...arr1];
看看結果
1 | console.log(arr1); // [1, 2, 3, {aa: 'aa'}] |
由於同上例子是屬於淺拷貝,結果是一樣的,這裡就簡單示範使用方法,不再作後續實驗。
兩者實際使用差異
Array.from
和Spread
在實際使用上還是有差異的,請參考這篇
Spread | Array.from |
---|---|
它不是運算符,僅適用於可迭代( iterable )的陣列 | 也適用於不可迭代的偽陣列( 具有 length 屬性和索引屬性的陣列 ) |
1 | const arrayLikeObject = { 0: 'a', 1: 'b', length: 2 }; |
結論是,當你想要將某東西轉換成陣列時,推薦使用 Array.from
,因為他更好閱讀。而當你想要連接多個陣列時,Spread
可能較適合,如 ['a', 'b', ...someArray, ...someOtherArray]
Object.assign
Object.assign
的可以傳入兩個參數,第一個參數可以是物件或是陣列,第二個參數為要合併的目標,若第一個參數帶入空物件或空陣列就可以達到淺拷貝的效果。
1 | var obj1 = { name: 'Dylan', family: { |
輸出看看,看似結果一樣
1 | console.log(obj1); |
但是由於淺拷貝的特性,第二層以後的物件還是會有傳參考的問題。
1 | console.log(obj1 === obj2); // false |
在來試著改變其中一個物件的第一層純值,結果並不會傳參考。
1 | obj2.name = 'Tony'; |
再來試著改變物件中的第二層物件,結果就不同了,形成傳參考,這也是淺拷貝的問題。
1 | obj2.family.dad = 'Walt'; |
深拷貝 (Deep Copy)
這邊一樣舉一些例子介紹
JSON.stringify 再 JSON.parse
我們可以使用 JSON.stringify
將陣列或物件轉換成字串後,再用 JSON.parse
轉回來。
1 | var obj1 = { name: 'Dylan', family: { |
1 | console.log(obj1 === obj2); // false |
即使修改第二層物件,也不會影響另一個陣列的內容
1 | obj2.family.dad = 'Walt'; |
這樣就是真正的 Deep Copy,也是這篇介紹唯一不需要使用其他函式庫的深拷貝方式,非常方便,但缺點是只有可以轉成 JSON 格式的物件才可以使用,例如 function
就沒有辦法被轉成 JSON。
1 | var obj1 = { fn: function(){ console.log('aaa') } }; |
驗證
1 | console.log(obj1.fn); // ƒ (){ console.log('aaa') } |
被複製物件內的 function
會消失,所以這個方法只能用在單純只有資料的物件。
jQuery
我們還可以使用 jQuery 的 $.extend
來做到深拷貝
1 | var obj1 = { name: 'Dylan', family: { |
驗證
1 | console.log(obj1 === obj2); // false |
lodash
這是一個非常多人在使用的函式庫,我們可以使用他所提供的 _.cloneDeep
來做到深拷貝,效能佳,且使用簡單易讀。
1 | var obj1 = { name: 'Dylan', family: { |
驗證
1 | console.log(obj1 === obj2); // false |
參考資料
[Javascript] 關於 JS 中的淺拷貝和深拷貝
關於JAVASCRIPT中的SHALLOW COPY(淺拷貝)及DEEP COPY(深拷貝)