JavaScript 物件深入淺出重點整理

資料型別 (Data Type)

JavaScript 的型別主要分兩大類別,分別是原始型別 (Primitive type) 及參考型別 (Reference type)。

  1. Primitive type
    • Boolean
    • Null
    • Undefined
    • Number
    • String
    • BigInt
    • Symbol(於 ECMAScript 6 新定義)
  2. Reference type
    • Object: Primitive type 以外的都屬於 Object 型別。例:Object {}Array []Function ()

Primitive type 和 Reference type 都是 call by value。不一樣的是儲存的東西。結束!

有人會說 Reference type 是 call by reference 或 call by sharing (這個比較符合我的意思和理解)。但是請先去看這篇:值 (value)、指標 (pointer/address)、參考 (reference)

Primitive types call by value

1
2
3
4
5
6
7
8
let x = 5, y = 10;
swap(x, y);

function swap(a, b){
let tmp = a;
a = b;
b = tmp;
}

call by value:將 x, y 的值 copy 一份給 a, b。

swap() 執行完畢後 x, y 不會互換,a, b 會互換。

Reference types call by value

1
2
3
4
5
6
7
8
9
let x = { name: "Jenifer" },
y = { name: "Mary" };
swap(x, y);

function swap(a, b){
let tmp = a;
a = b;
b = tmp;
}

call by value:一樣將 x, y 的值 copy 一份給 a, b。但是,因為儲存的不是物件本身,而是物件的位址,所以有很多陷阱和地雷要注意。

swap() 執行完畢後 x, y 不會互換,a, b 會互換。

如果改成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let x = { name: "Jenifer" },
y = { name: "Mary" };
let a = x,
b = y;
swap();

console.log(b);

function swap(){
let tmp = a;
a = b;
b = tmp;

x.isChild = false; //在同樣的記憶體位址增加屬性
}

// 輸出結果:
// {name: "Jenifer", isChild: false}

使用 物件名稱.屬性名稱 = 值 會在同樣的記憶體位址增加屬性。因為 bx 儲存一樣的記憶體位址,所以印出和 x 相同的結果。

使用 const 還是可以對屬性修改

因為沒有修改儲存的記憶體位址,僅修改屬性,不會報錯。注意:物件中的屬性名稱只會唯一存在。

1
2
3
4
5
6
7
const x = { name: "Jenifer" };
x.name = "Alice"; //屬性名稱只會唯一存在,"Alice" 會覆蓋 "Jenifer"
x.isChild = true;
console.log(x);

// 輸出結果:
// {name: "Alice", isChild: true}

重新指定記憶體位址會報錯。

splice()pop()push() 屬於修改屬性的函式。
slice()回傳一個新的陣列的函式。

1
2
3
4
5
6
7
8
9
10
function newStaff(obj) {
const newPerson = { name: "Alice" };
obj = newPerson;
}

const x = { name: "Jenifer" };
newStaff(x);
console.log(x);

// {name: 'Jenifer'}

利用 Object.keys() 淺層複製

The Object.keys() method returns an array of a given object’s own enumerable property names, iterated in the same order that a normal loop would.

Object.keys() 回傳一個新的陣列,依序列舉出屬性名稱

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const x = { 
name: "Jenifer",
age: 100,
isChild: false,
sayHi: function (name){
console.log("Hello " + name);
}
};

let new1 = { ...x }
console.log( new1 === x );

let new2 = Object.assign({}, x)
console.log( new2 === x );

const newX = {};
Object.keys(x).forEach((key)=>{
newX[key] = x[key];
});

x.sayHi(x.name);
console.log(Object.keys(x));
console.log(Object.keys(newX));

// 輸出結果:
// "Hello Jenifer"
// ['name', 'age', 'isChild', 'sayHi']
// ['name', 'age', 'isChild', 'sayHi']

注意:像是陣列的物件,有著任意 key 順序,使用 Object.keys() 會自動將其排序。

array-like object with random key ordering

1
2
3
4
5
6
7
8
9
const fruits = { 
90:"apple",
13:"orange",
65:"mango"
};
console.log(Object.keys(fruits));

// 輸出結果:
// ['13', '65', '90']

參考資料:
MDN Web Docs - Object.keys()

淺層複製

1. 展開運算子 (Spread Operator) { ...obj }[ ...obj ]

淺層會有新的記憶體位址,但是深層的記憶體位址就一樣。改動新物件時會更動到舊物件,不是好的複製。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const x = { 
name: "Jenifer",
age: 100,
isChild: false,
favoriteFruits: {
Apple: 94,
Orange: 75,
Mango: {
yellow: 100,
red: 89,
green: 10
}
}
};

const newX = { ...x }; // 展開

console.log(newX === x); // false,記憶體位址不同
console.log(newX.favoriteFruits === x.favoriteFruits); // true,記憶體位址相同

2. 會回傳一個新物件、陣列的方法

a. Object.keys()for-in loop
b. 僅 Array: Array.map( x => x )Array.filter(() => true) 等。
c. 僅 Object: Object.assign({}, obj) (array 也可以用,但會變物件)。對於 obj 來說 Object.assign({}, obj){ ...obj } 基本上是一樣的。

深層複製

1. JSON.parse(JSON.stringify(obj))

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const x = { 
name: "Jenifer",
age: 100,
isChild: false,
favoriteFruits: {
Apple: 94,
Orange: 75,
Mango: {
yellow: 100,
red: 89,
green: 10
}
}
};

const newX = JSON.parse(JSON.stringify(x)); // 展開

console.log(newX === x); // false,記憶體位址不同
console.log(newX.favoriteFruits === x.favoriteFruits); // false,記憶體位址不同

2. structuredClone(obj)

ECMAScript 2021 新提供了 structuredClone(),這是一個用於深層複製的內建函式。

1
2
3
4
const newX2 = structuredClone(x);

console.log(newX2 === x); // false,記憶體位址不同
console.log(newX2.favoriteFruits === x.favoriteFruits); // false,記憶體位址不同

補充資料:
structuredClone()
Can I use structuredClone?

奇怪的物件

1
2
3
4
5
6
7
8
9
10
let a = {
name: "Jenifer"
}
let b = a;

console.log(a.y, b.y); // undefined undefined

a.y = a = {
name: "Mary"
};

從第六行可以看出來有 Hoisting 的特性,a.y 或是說 b.y 早就已經被創造好,被賦予 undefined。因此在第六行時,情況如下圖。

第八行 a 被賦予新物件。而 a = { name: "Mary" } 是表達式,會回傳 { name: "Mary" } ,因此 a.y 或是說 b.y 也被賦予新物件。

參考資料:
六角學院:JavaScript 常見考題破解:物件傳值?傳參考?