陣列與物件
物件型別 (object type) 包含:
- array
- object
- function
用來組織各種原始型別 (primitive type) 的資料。
call by value
原始型別在進行重新賦值時,是對純值複製一份,再賦予回去,也稱為 call by value:
let a = 1;let b = a;
a = 2;console.log(a); // 2console.log(b); // 1call by sharing
物件型別在重新賦值時,是複製記憶體位址,而不是裡面的內容。
因為物件與陣列的內容不是固定大小的,所以宣告物件型別時通常是標註一個記憶體位置,而裡面的元素或程式碼則根據程式運行時需要的大小進行分配並且指向這個記憶體位置。
let arr1 = [];let arr2 = arr1;
arr1.push(1);arr1.push(2);
// arr2 存的是 arr1 的記憶體位址,所以查看 arr2 等同於 arr1console.log(arr2); // [1, 2];由上面範例可以看到,雖然 arr2 是從 arr1 複製的,但是對 arr1 新增了資料後,arr2 的值也發生改變了!
這種情況稱為 call by sharing。
物件型別的比對也不是看內容 (shape),而是記憶體位址,所以寬鬆比較的結果還是 false:
let arr1 = [];let arr2 = [];
console.log(arr1 == arr2); // false
let obj1 = {};let obj2 = {};
console.log(obj1 == obj2); // falseconst 宣告
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'透過參數接到 data,等同於在函式內宣告變數 obj 然後複製 data 過來,所以 obj.name 同時改變到原本的 data 裡的 name 了。
但是對 obj 重新賦值不會改變原始變數 data 所指向的記憶體位址,所以最後印出來的 name 仍然是 Vic。
如果沒有特殊需求,物件型別一般都是用 const 宣告,語意上表示這是「不可任意改變位址」的物件,也可以防止在撰寫的過程中意外改寫內容。
淺拷貝 (Shallow Copy)
知道 call by sharing 後,就產生了一個問題:
「到底應該怎麼複製物件的內容而且形成一個獨立的物件?」
直接賦值的方式也叫做淺拷貝,想要形成獨立的物件就只能透過其他方式:
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 }]展開運算子只能重新分配第一層元素的記憶體,如果物件內容裡面又是另一層物件,第二層以後還是會指向原本的記憶體空間,所以展開運算還是一種淺拷貝。
不考慮其他工具的話,最簡單的方法是 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}]把原本的內容強制轉型成字串,再還原成物件的格式,形成全新的記憶體空間。