跳至主要内容

陣列與物件

9/6/2023 發布

number、string、null 等等是為「原始型別」(primitive type),內容通常是純值。

object、array 則為「物件型別」(object type),
它們比較像是箱子,裡面可以包含各種型別的資料。

注意陣列是一種 object,所以用 typeof 檢查不出來內容到底是否為陣列,
NaN 一樣需要用其他方法判定:

let arr = [];

console.log(arr === []); // false
console.log(Array.isArray(arr)); // true

call by value

原始型別在存取時時只會拷貝到值,函式在帶入這些變數時,
實際上是把這些值複製一份到函式裡面使用,這種情況稱為 call by value

let a = 1;
let b = a;

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

call by sharing

物件型別在在存取時是把記憶體位址拷貝過去
不是裡面的內容,如 arr1 = [1],所以 arr1 存的是一個記憶體位址 0x01
這個位址裡面放的才是陣列的內容:

let arr1 = [];
let arr2 = arr1;

arr1.push(1);
arr1.push(2);

// arr2 存的是 arr1 的記憶體位址,所以查看 arr2 等同於 arr1
console.log(arr2); // [1, 2];

由上面範例可以看到,雖然 arr2 拷貝了 arr1,
但是對 arr1 新增了資料後,arr2 的值居然也發生改變了!
這種情況稱為 call by sharing

在進行比對時,物件並不等於物件,因為它們比對的並不是值或內容,
而是記憶體位址,所以寬鬆比較的結果還是 false:

let arr1 = [];
let arr2 = [];

console.log(arr1 == arr2); // false

let obj1 = {};
let obj2 = {};

console.log(obj1 == obj2); // false

const 宣告

sharing 代表可以共享物件內的屬性,所以在剛剛的範例裡,
arr1 = arr2 拷貝之後去操作 arr2.push,實際上是在執行 arr1.push

透過參數傳進函式內的話會有點不一樣:

let data = { name: "Jack" };

function changeContent(obj) {
obj.name = "Vic";
console.log(obj.name); // 'Vic'

obj = { name: "Merry" };
console.log(obj.name); // 'Merry'
}

changeContent(data);
console.log(data.name); // 'Vic'

透過參數接到該物件,等同於在函式內宣告變數然後拷貝該物件,
所以 obj.name 同時改變到原本的 data 裡的 name 了,
但是重新賦值並不會改變原始變數所指向的記憶體位址
所以最後印出來的 name 仍然是 Vic。

如果沒有清空內容等等的特殊需求,物件型別一般都是用 const 宣告,
語意上可以表示這是「不可任意改變位址綁定」的物件,
也可以防止在撰寫的過程中意外改寫內容。


淺拷貝

知道 call by sharing 後,就產生了一個問題:

到底應該怎麼複製物件的內容而且形成一個獨立的物件?」

直接賦值的拷貝也叫做淺拷貝(shallow copy)
想要形成獨立的物件就只能透過其他方式:

let arr1 = [1, 2];
let arr2 = [...arr1];

arr1.push(3);

console.log(arr2); // [1, 2]

使用展開運算子看起來似乎沒問題,arr2 的值並沒有被 arr1 的行為影響,
但這只限在第一層而已:

let arr1 = [{ a: 1 }];
let arr2 = [...arr1];

arr1[0].a = 2;
console.log(arr2); // [{ a : 2 }]

展開運算子只能重新分配第一層的記憶體,
如果物件內容裡面又是另一層的物件,
第二層以後還是會指向原本的記憶體空間,
所以展開運算還是一種淺拷貝。

不考慮其他工具的話,要達成深拷貝(deep copy)的方法是 JSON 字串轉換

let arr1 = [{ a: 1 }, { b: 2 }];
let arr2 = JSON.parse(JSON.stringify(arr1));

arr1[1].b = 0;

console.log(arr2); // [{ a : 1 },{ b : 2}]

把原本的內容強制轉型成字串, 再還原成物件的格式,形成一個全新的記憶體空間。


參考資料