我應該使用 Object 還是 Map?
前言
在日常的 JavaScript 項目中,我們最常用到的數據結構就是各種形式的鍵值對格式了(key-value pair)。在 JavaScript 中,除了最基礎的 Object
是該格式外,ES6 新增的 Map
也同樣是鍵值對格式。它們的用法在很多時候都十分接近。不知道有沒有人和我一樣糾結過該選擇哪個去使用呢?在本菜最近的項目中,我又遇到了這樣的煩惱,索性一不做二不休,去對比一下究竟該使用哪一個。
本文將會探討一下 Object
和 Map
的不同,從多個角度對比一下 Object
和 Map
:
-
用法的區別:在某些情況下的用法會截然不同
-
句法的區別:創建以及增刪查改的句法區別
-
性能的區別:速度和內存佔用情況
希望讀完本文的你可以在日後的項目中做出更爲合適的選擇。
用法對比
-
對於
Object
而言,它鍵(key)的類型只能是字符串,數字或者Symbol
;而對於Map
而言,它可以是任何類型。(包括 Date,Map,或者自定義對象) -
Map
中的元素會保持其插入時的順序;而Object
則不會完全保持插入時的順序,而是根據如下規則進行排序:
-
非負整數會最先被列出,排序是從小到大的數字順序
-
然後所有字符串,負整數,浮點數會被列出,順序是根據插入的順序
-
最後纔會列出
Symbol
,Symbol
也是根據插入的順序進行排序的
-
讀取
Map
的長度很簡單,只需要調用其.size()
方法即可;而讀取Object
的長度則需要額外的計算:Object.keys(obj).length
-
Map 是可迭代對象,所以其中的鍵值對是可以通過
for of
循環或.foreach()
方法來迭代的;而普通的對象鍵值對則默認是不可迭代的,只能通過for in
循環來訪問(或者使用Object.keys(o)、Object.values(o)、Object.entries(o)
來取得表示鍵或值的數字)迭代時的順序就是上面提到的順序。const o = {}; const m = new Map(); o[Symbol.iterator] !== undefined; // false m[Symbol.iterator] !== undefined; // true
-
在
Map
中新增鍵時,不會覆蓋其原型上的鍵;而在Object
中新增鍵時,則有可能覆蓋其原型上的鍵:Object.prototype.x = 1; const o = {x:2}; const m = new Map([[x,2]]); o.x; // 2,x = 1 被覆蓋了 m.x; // 1,x = 1 不會被覆蓋
-
JSON
默認支持Object
而不支持Map
。若想要通過JSON
傳輸Map
則需要使用到.toJSON()
方法,然後在JSON.parse()
中傳入復原函數來將其復原。對於
JSON
這裏就不具體展開了,有興趣的朋友可以看一下這:JSON 的序列化和解析const o = {x:1}; const m = new Map([['x', 1]]); const o2 = JSON.parse(JSON.stringify(o)); // {x:1} const m2 = JSON.parse(JSON.stringify(m)) // {}
句法對比
創建時的區別
Obejct
const o = {}; // 對象字面量
const o = new Object(); // 調用構造函數
const o = Object.create(null); // 調用靜態方法 Object.create
對於 Object
來說,我們在 95%+ 的情況下都會選擇對象字面量,它不僅寫起來最簡單,而且相較於下面的函數調用,在性能方面會更爲高效。對於構建函數,可能唯一使用到的情況就是顯式的封裝一個基本類型;而 Object.create
可以爲對象設定原型。
Map
const m = new Map(); // 調用構造函數
和 Object
不同,Map
沒有那麼多花裏胡哨的創建方法,通常只會使用其構造函數來創建。
除了上述方法之外,我們也可以通過 Function.prototype.apply()、Function.prototype.call()、reflect.apply()、Reflect.construct()
方法來調用 Object
和 Map
的構造函數或者 Object.create()
方法,這裏就不展開了。
新增 / 讀取 / 刪除元素時的區別
Obejct
const o = {};
//新增/修改
o.x = 1;
o['y'] = 2;
//讀取
o.x; // 1
o['y']; // 2
//或者使用 ES2020 新增的條件屬性訪問表達式來讀取
o?.x; // 1
o?.['y']; // 2
//刪除
delete o.b;
對於新增元素,看似使用第一種方法更爲簡單,不過它也有些許限制:
-
屬性名不能包含空格和標點符號
-
屬性名不能以數字開頭
對於條件屬性訪問表達式的更多內容可以看一下這:條件屬性訪問表達式
Map
const m = new Map();
//新增/修改
m.set('x', 1);
//讀取
map.get('x');
//刪除
map.delete('b');
對於簡單的增刪查改來說,Map
上的方法使用起來也是十分便捷的;不過在進行聯動操作時,Map
中的用法則會略顯臃腫:
const m = new Map([['x',1]]);
// 若想要將 x 的值在原有基礎上加一,我們需要這麼做:
m.set('x', m.get('x') + 1);
m.get('x'); // 2
const o = {x: 1};
// 在對象上修改則會簡單許多:
o.x++;
o.x // 2
性能對比
接下來我們來討論一下 Object
和 Map
的性能。不知道各位有沒有聽說過 Map 的性能優於 Object 的說法,我反正是見過不少次,甚至在 JS 高程四中也提到了 Map
對比 Object
時性能的優勢;不過對於性能的概括都十分的籠統,所以我打算做一些測試來對比一下它們的區別。
測試方法
在這裏我進行的對於性能測試的都是基於 v8 引擎的。速度會通過 JS 標準庫自帶的 performance.now()
函數來判斷,內存使用情況會通過 Chrome devtool
中的 memory
來查看。
對於速度測試,因爲單一的操作速度太快了,很多時候 performance.now()
會返回 0。所以我進行了 10000 次的循環然後判斷時間差。因爲循環本身也會佔據一部分時間,所以以下的測試只能作爲一個大致的參考。
創建時的性能
測試用的代碼如下:
let n, n2 = 5;
// 速度
while (n2--) {
let p1 = performance.now();
n = 10000;
while (n--) { let o = {}; }
let p2 = performance.now();
n = 10000;
while (n--) { let m = new Map(); }
let p3 = performance.now();
console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 內存
class Test {}
let test = new Test();
test.o = o;
test.m = m;
首先進行對比的是創建 Object
和 Map
時的表現。對於創建的速度表現如下:
我們可以發現創建 Object
的速度會快於 Map
。對於內存使用情況則如下:
我們主要關注其 Retained Size
,它表示了爲其分配的空間。(即刪除時釋放的內存大小)
通過對比我們可以發現,空的 Object
會比空的 Map
佔用更少的內。所以這一輪 Object
贏得一籌。
新增元素時的性能
測試用的代碼如下:
console.clear();
let n, n2 = 5;
let o = {}, m = new Map();
// 速度
while (n2--) {
let p1 = performance.now();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
let p2 = performance.now();
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p3 = performance.now();
console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 內存
class Test {}
let test = new Test();
test.o = o;
test.m = m;
對於新建元素時的速度表現如下:
我們可以發現新建元素時,Map
的速度會快於 Object
。對於內存使用情況則如下:
通過對比我們可以發現,在擁有一定數量的元素時, Object
會比 Map
佔用多了約 78% 的內存。我也進行了多次的測試,發現在擁有足夠的元素時,這個百分比是十分穩定的。所以說,在需要進行很多新增操作,且需要儲存許多數據的時候,使用 Map
會更高效。
讀取元素時的性能
測試用的代碼如下:
let n;
let o = {}, m = new Map();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p1 = performance.now();
for (key in o) { let k = o[key]; }
let p2 = performance.now();
for ([key] of m) { let k = m.get(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`
對於讀取元素時的速度表現如下:
通過對比,我們可以發現 Object
略佔優勢,但總體差別不大。
刪除元素時的性能
不知道大家是否聽說過 delete
操作符性能低下,甚至有很多時候爲了性能,會寧可將值設置爲 undefined
而不使用 delete
操作符的說法。但其實在 v8
近來的優化下,它的效率已經提升許多了。
測試用的代碼如下:
let n;
let o = {}, m = new Map();
n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }
let p1 = performance.now();
for (key in o) { delete o[key]; }
let p2 = performance.now();
for ([key] of m) { m.delete(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`
對於刪除元素時的速度表現如下:
我們可以發現在進行刪除操作時,Map
的速度會略佔優,但整體差別其實並不大。
特殊情況
其實除了最基本的情況之外,還有一種特殊的情況。還記得我們在前面提到的 Object
中鍵的排序嗎?我們提到了其中的非負整數會被最先列出。其實對於非負整數作爲鍵的值和其餘類型作爲鍵的值來說,v8
是會對它們進行區別對待的。負整數作爲鍵的部分會被當成數組對待,即非負整數具有一定的連續性時,會被當成快數組,而過於稀疏時會被當成慢數組。
對於快數組,它擁有連續的內存,所以在進行讀寫時會更快,且佔用更少的內存。更多的內容可以看一下這: 探究 JS V8 引擎下的 “數組” 底層實現
在鍵爲連續非負整數時,性能如下:
我們可以看到 Object
不僅平均速度更快了,其佔用的內存也大大減少了。
總結
通過對比我們可以發現,Map
和 Object
各有千秋,對於不同的情況下,我們應當作出不同的選擇。所以我總結了一下我認爲使用 Map
和 Object
更爲合適的時機。
使用 Map
:
-
儲存的鍵不是字符串 / 數字 / 或者
Symbol
時,選擇Map
,因爲Object
並不支持 -
儲存大量的數據時,選擇
Map
,因爲它佔用的內存更小 -
需要進行許多新增 / 刪除元素的操作時,選擇
Map
,因爲速度更快 -
需要保持插入時的順序的話,選擇
Map
,因爲Object
會改變排序 -
需要迭代 / 遍歷的話,選擇
Map
,因爲它默認是可迭代對象,迭代更爲便捷
使用 Object
:
-
只是簡單的數據結構時,選擇
Object
,因爲它在數據少的時候佔用內存更少,且新建時更爲高效 -
需要用到
JSON
進行文件傳輸時,選擇Object
,因爲JSON
不默認支持Map
-
需要對多個鍵值進行運算時,選擇
Object
,因爲句法更爲簡潔 -
需要覆蓋原型上的鍵時,選擇
Object
雖然 Map
在很多情況下會比 Object
更爲高效,不過 Object
永遠是 JS
中最基本的引用類型,它的作用也不僅僅是爲了儲存鍵值對。
參考
探究 JS V8 引擎下的 “數組” 底層實現
Fast properties in V8
Shallow, Retained, and Deep Size
Slow delete of object properties in JS in V8
ES6 — Map vs Object — What and when?
JavaScript 高級程序設計(第 4 版)
JavaScript: The Definitive Guide (7th Edition)
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/TKBEqoULaDkWaHsa25hRVQ