這是介紹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中最基礎的概念。
- 如果你知道所有的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入門 - 阮一峰