為什麼需要 Map?
在 Map 被引入之前,JavaScript 開發者主要使用 普通物件 (Plain Object) 來作為「鍵值對 (Key-Value Pair)」的對應表結構。然而,普通物件在擔任這個角色時有兩個主要的限制,這正是 ECMAScript 委員會決定創建 Map 的原因:
1. 繼承的原型屬性問題
每個 JavaScript 普通物件都會從它的原型 (Prototype) 繼承屬性。
-
問題所在: 當您嘗試使用物件來儲存資料時,您可能會意外地讀取或覆蓋從原型鏈上繼承來的屬性,例如
toString或constructor。這會導致資料上的命名衝突 (Name Collision) 或非預期的行為。 -
舉例說明: 即使您沒有明確地將鍵設定為
'toString',當您檢查obj['toString']時,您仍會得到一個繼承來的函式,而不是您預期中的undefined或您自己的值。要避免這個問題,開發者必須使用像Object.create(null)來創建「空原型」的物件,或是使用hasOwnProperty()進行額外的檢查,非常麻煩。
2. 鍵的資料型態限制
普通物件只能使用 字串 (String) 或 Symbol 作為鍵 (Key)。
-
問題所在:
-
如果您使用數字 (Number)、布林值 (Boolean) 或任何其他非字串的基本型態作為鍵,JavaScript 會自動將它們強制轉換 (Coerce) 成字串。例如,
obj[5]和obj['5']實際上是同一個鍵。 -
更重要的限制是,您不能使用物件或函式作為鍵。如果您嘗試這樣做,它們會被強制轉換成字串,例如
"[object Object]",這使得所有物件鍵都變成了同一個字串鍵,導致您無法區分不同的物件實例。
-
-
舉例說明:
let a = {id: 1};
let b = {id: 2};
let myObj = {};
myObj[a] = 'Value A';
myObj[b] = 'Value B';
console.log(myObj); // 輸出:{ '[object Object]': 'Value B' }
// myObj[a] 和 myObj[b] 實際上都是在設定名為 '[object Object]' 的同一個屬性!
Map 如何解決這些問題?
Map (正式名稱為 Map 物件) 是一種新的集合型態,它專門被設計來作為純粹的鍵值對應表,徹底解決了上述兩個限制:
1. 沒有原型繼承問題
-
Map 是一個獨立的資料結構,不會從任何原型繼承屬性。
-
您儲存的每一個鍵值對都是 Map 專屬的,不會受到任何預設屬性 (如
toString、constructor) 的干擾。這使得 Map 成為一個「乾淨」的對應表。
2. 任何資料型態都可以作為鍵
-
Map 允許您使用任何型態的值作為鍵,包括:
-
物件 (Objects)
-
函式 (Functions)
-
數字 (Numbers)
-
布林值 (Booleans)
-
null和undefined
-
-
當您使用物件作為鍵時,Map 會區分不同的物件實例。這意味著您可以使用一個 DOM 元素或一個類別實例作為鍵,並將資料「附著」在它上面。
let a = {id: 1};
let b = {id: 2};
let myMap = new Map();
myMap.set(a, 'Value A'); // 'a' 物件本身是鍵
myMap.set(b, 'Value B'); // 'b' 物件本身是另一個鍵
console.log(myMap.get(a)); // 輸出:'Value A'
console.log(myMap.get(b)); // 輸出:'Value B'
console.log(myMap.size); // 輸出:2
結論
總而言之,Map 的設計目標是提供一個專門且可靠的鍵值儲存機制,避免了普通物件作為對應表時的原型污染和鍵型態限制。
| 特性 | 普通物件 (Plain Object) | Map |
|---|---|---|
| 鍵的型態 | 只能是字串或 Symbol | 任何型態 (物件、函式、數字等) |
| 原型繼承 | 會繼承原型鏈上的屬性 | 不會繼承,是乾淨的鍵值對應 |
| 鍵值計算 | 必須手動計算或使用迴圈 | 內建 size 屬性可直接取得 |
| 迭代順序 | 數字鍵會優先排序 | 依照鍵被插入的順序進行迭代 |
因此,在現代 JavaScript 開發中,如果您需要一個可靠的、高性能的、並且需要使用非字串鍵的對應表,Map 已經成為首選的資料型態。