【譯】Object與Map的異同及使用場景

EniviaQ發表於2019-03-07

這是介紹Array,Set,Object,Map系列的第二篇譯文。
原文連結:戳這裡
按照慣例,詳細的API補充在在文章底部。

【譯】Object與Map的異同及使用場景

你可能想問,為什麼要單獨將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中最基礎的概念。

  • 如果你知道所有的key,它們都為字串或整數(或是Symbol型別),你需要一個簡單的結構去儲存這些資料,Object是一個非常好的選擇。構建一個Object並通過知道的特定key獲取元素的效能要優於Map(字面量 vs 建構函式,直接獲取 vs get()方法)。
  • 如果需要在物件中保持自己獨有的邏輯和屬性,只能使用Object。
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並不能實現這樣的資料結構)

  • JSON直接支援Object,但尚未支援Map。因此,在某些我們必須使用JSON的情況下,應將Object視為首選。
  • Map是一個純雜湊結構,而Object不是(它擁有自己的內部邏輯)。使用delete對Object的屬性進行刪除操作存在很多效能問題。所以,針對於存在大量增刪操作的場景,使用Map更合適。
  • 不同於Object,Map會保留所有元素的順序。Map結構是在基於可迭代的基礎上構建的,所以如果考慮到元素迭代或順序,使用Map更好,它能夠確保在所有瀏覽器中的迭代效能。
  • Map在儲存大量資料的場景下表現更好,尤其是在key為未知狀態,並且所有key和所有value分別為相同型別的情況下。

總結

如何選擇Object和Map取決於你要使用的資料型別以及操作。

當我們只需要一個簡單的可查詢儲存結構時,Map相比Object更具優勢,它提供了所有基本操作。但在任何意義上,Map都不能替代Object。因為在Javascript中,Object畢竟不僅僅是一個普通的雜湊表(因此Object不應該用作普通雜湊表,它會浪費很多資源)。

其他資料連結:
Object - MDN - Mozilla
Map - MDN - Mozilla
ES6入門 - 阮一峰

相關文章