【譯】Object與Map的異同及使用場景
這是介紹Array,Set,Object,Map系列的第二篇譯文。
原文連結:戳這裡
按照慣例,詳細的API補充在在文章底部。

你可能想問,為什麼要單獨將Object和Map進行對比,而不是對比Map,Array,或是Object和Set?不同於其它兩組,Map和Object有非常多相似的地方需要我們去更深入的瞭解和對比,才能分析出他們分別更適合的應用場景。
概念
什麼是Map
Map是一種資料結構(它很特別,是一種抽象的資料結構型別),資料一對對進行儲存,其中包含鍵以及對映到該鍵的值。並且由於鍵的唯一性,因此不存在重複的鍵值對。
Map便是為了快速搜尋和查詢資料而生的。
例如: {(1, "smile"), (2, "cry"), (42, "happy")}
在Map中,每一對資料的格式都為鍵值對的形式。
注:Map中的鍵和值可以是任何資料型別,不僅限於字串或整數。
什麼是Object
JavaScript中的 常規物件 是一種字典型別的資料結構——這意味著它依然遵循與Map型別相同鍵值對的儲存結構。Object中的key,或者我們可以稱之為屬性,同樣是獨一無二的並且對應著一個單獨的value。
另外,JavaScript中的Object擁有內建原型(prototype)。需要注意的是,JavaScript中幾乎所有物件都是Object例項,包括Map。
例如: {1: 'smile', 2: 'cry', 42: 'happy'}
從定義上來看,Object和Map的本質都是以鍵值對的方式儲存資料,但實質上他們之間存在很大的區別——
- 鍵:Object遵循普通的字典規則,鍵必須是單一型別,並且只能是整數、字串或是Symbol型別。但在Map中,key可以為任意資料型別(Object, Array等)。(你可以嘗試將一個物件設定為一個Object的key,看看最終的資料結構)
- 元素順序:Map會保留所有元素的順序,而Object並不會保證屬性的順序。(如有疑問可參考:連結)
- 繼承:Map是Object的例項物件,而Object顯然不可能是Map的例項物件。
var map = new Map([[1,2],[3,4]]); console.log(map instanceof Object); //true var obj = new Object(); console.log(obj instanceof Map); //false 複製程式碼
如何構建
Object
與陣列相似,定義一個Object的方式非常簡單直接:
var obj = {}; //空物件 var obj = {id: 1, name: "Test object"}; //2 keys here: id maps to 1, and name maps to "Test object" 複製程式碼
或使用構造方法:
var obj = new Object(); //空物件 var obj = new Object; //空物件 複製程式碼
或者使用 Object.prototype.create
var obj = Object.create(null); //空物件 複製程式碼
注:
你只能在某些特定的情況下使用 Object.prototype.create
,比如:
- 你希望繼承某個原型物件,而無需定義它的建構函式。
var Vehicle = { type: "General", display: function(){console.log(this.type);} } var Car = Object.create(Vehicle); //建立一個繼承自Vehicle的物件Car Car.type = "Car";//重寫type屬性 Car.display(); //Car Vehicle.display(); //General 複製程式碼
在通常情況下,與陣列相似,儘量避免使用建構函式的方式,理由如下:
- 建構函式會寫更多程式碼
- 效能更差
- 更加混亂更容易引起程式錯誤,例如:
var obj = new Object(id: 1, name: "test") //顯然的語法錯誤 var obj1 = {id: 1, name: "test"}; var obj2 = new Object(obj1); //obj1與obj2指向同一個物件 obj2.id = 2; console.log(obj1.id); //2 複製程式碼
Map
建立Map只有一種方式,就是使用其內建的建構函式以及 new
語法。
var map = new Map(); //Empty Map var map = new Map([[1,2],[2,3]]); // map = {1=>2, 2=>3} 複製程式碼
語法:
Map([iterable])
Map的建構函式接收一個數組或是一個可遍歷的物件作為引數,這個引數內的資料都為鍵值對結構。如果是陣列,則包含兩個元素 [key, value]
。
訪問元素
- 對於Map,獲取元素要通過方法
Map.prototype.get(key)
實現,這意味著我們必須先知道該值所對應的key
map.get(1); 複製程式碼
- Object類似,要獲取到值必須先知道所對應的key/property,不過使用不同的語法:
Object.<key> and Object[‘key’]
obj.id //1 obj['id'] //1 複製程式碼
- 判斷Map中是否存在某個key
map.has(1);//return boolean value:true/false 複製程式碼
- Object則需要一些額外的判斷
var isExist = obj.id === undefined; // or var isExist = 'id' in obj; // 該方法會檢查繼承的屬性 複製程式碼
Map與Object語法很相似,不過Map的語法更簡單。
注:我們可以使用 Object.prototype.hasOwnProperty()
判斷Object中是否存在特定的key,它的返回值為 true/false
,並且 只會檢查物件上的非繼承屬性 。
插入元素
- Map支援通過
Map.prototype.set()
方法插入元素,該方法接收兩個引數:key,value。如果傳入已存在的key,則將會重寫該key所對應的value。
map.set(4,5); 複製程式碼
- 同樣,為Object新增屬性可以使用下面的方法
obj['gender'] = 'female'; //{id: 1, name: "test", gender: "female"} obj.gender = male; // 重寫已存在的屬性 //{id: 1, name: "test", gender: "male"} 複製程式碼
正如你所看到的,歸功於其資料結構,兩種插入元素方法的時間複雜度都為O(1),檢索key並不需要遍歷所有資料。
刪除元素
Object並沒有提供刪除元素的內建方法,我們可以使用 delete
語法:
delete obj.id; 複製程式碼
值得注意的是,很多人提出使用一下方法是否會更好,更節約效能。
obj.id = undefined 複製程式碼
這兩種方式在邏輯上有很大差別:
-
delete
會完全刪除Object上某個特有的屬性 - 使用
obj[key] = undefined
只會改變這個key所對應的value為undefined
,而該屬性仍然保留在物件中。
因此在使用 for...in...
迴圈時仍然會遍歷到該屬性的key。
當然,檢查Object中是否已存在某屬性將在這兩種情況下產生兩種不同的結果,但以下檢查除外:
obj.id === undefined; //結果相同 複製程式碼
因此,效能提升在某些情況下並不適合。
還有一點, delete
操作符的返回值為 true/false
,但其返回值的依據與預想情況有所差異:
對於所有情況都返回 true
,除非屬性是一個 non-configurable
屬性,否則在非嚴格模式返回 false
,嚴格模式下將丟擲異常。
Map有更多內建的刪除元素方式,比如:
-
delete(key)
用於從Map中刪除特定key所對應的value,該方法返回一個布林值。如果目標物件中存在指定的key併成功刪除,則返回true
;如果物件中不存在該key則返回false
。
var isDeleteSucceeded = map.delete(1); console.log(isDeleteSucceeded); //true- 複製程式碼
-
clear()
——清空Map中所有元素。
map.clear(); 複製程式碼
Object要實現Map的 clear()
方法,需要遍歷這個物件的屬性,逐個刪除。
Object和Map刪除元素的方法也非常相似。其中刪除某個元素的時間複雜度為O(1),清空元素的時間複雜度為O(n),n為Object和Map的大小。
獲取大小
與Object相比,Map的一個優點是它可以自動更新其大小,我們可以通過以下方式輕鬆獲得:
console.log(map.size); 複製程式碼
而使用Object,我們需要通過 Object.keys()
方法計算其大小,該方法返回一個包含所有key的陣列。
console.log(Object.keys(obj).length); 複製程式碼
元素的迭代
Map有內建的迭代器,Object沒有內建的迭代器。
補充:如何判斷某種型別是否可迭代,可以通過以下方式實現
//typeof <obj>[Symbol.iterator] === “function” console.log(typeof obj[Symbol.iterator]); //undefined console.log(typeof map[Symbol.iterator]); //function 複製程式碼
在Map中,所有元素可以通過 for...of
方法遍歷:
//For map: { 2 => 3, 4 => 5 } for (const item of map){ console.log(item); //Array[2,3] //Array[4,5] } //Or for (const [key,value] of map){ console.log(`key: ${key}, value: ${value}`); //key: 2, value: 3 //key: 4, value: 5 } 複製程式碼
或者使用其內建的 forEach()
方法:
map.forEach((value, key) => console.log(`key: ${key}, value: ${value}`)); //key: 2, value: 3 //key: 4, value: 5 複製程式碼
但對於Object,我們使用 for...in
方法
//{id: 1, name: "test"} for (var key in obj){ console.log(`key: ${key}, value: ${obj[key]}`); //key: id, value: 1 //key: name, value: test } 複製程式碼
或者使用 Object.keys(obj)
只能獲取所有key並進行遍歷
Object.keys(obj).forEach((key)=> console.log(`key: ${key}, value: ${obj[key]}`)); //key: id, value: 1 //key: name, value: test 複製程式碼
好的,問題來了,因為它們在結構和效能方面都非常相似,Map相比Object具有更多的優勢,那我們是否應該更常使用Map代替Object?
Object和Map的應用場景
儘管,Map相對於Object有很多優點,依然存在某些使用Object會更好的場景,畢竟Object是JavaScript中最基礎的概念。
get()
var obj = { id: 1, name: "It's Me!", print: function(){ return `Object Id: ${this.id}, with Name: ${this.name}`; } } console.log(obj.print());//Object Id: 1, with Name: It's Me. 複製程式碼
(你可以使用Map進行嘗試,然而Map並不能實現這樣的資料結構)
delete
總結
如何選擇Object和Map取決於你要使用的資料型別以及操作。
當我們只需要一個簡單的可查詢儲存結構時,Map相比Object更具優勢,它提供了所有基本操作。但在任何意義上,Map都不能替代Object。因為在Javascript中,Object畢竟不僅僅是一個普通的雜湊表(因此Object不應該用作普通雜湊表,它會浪費很多資源)。