如何利用JavaScript的Map提高性能

在ES6中引入JavaScript的新特性中,咱們看到了SetMap的介紹。與常規對象和Array不一樣的是,它們是「鍵控集合(keyed collections)」。這就是說它們的行爲有稍許不一樣,而且在特定的上下文中使用,它們能夠提供至關大的性能優點。javascript

在這篇文章中,我將剖析Map,它究竟有何不一樣,哪裏能夠派上用場,相比於常規對象有什麼性能優點。java

Map與常規對象有什麼不一樣

Map和常規對象主要有2個不一樣之處。數組

1.無限制的鍵(Key)

常規JavaScript對象的鍵必須是StringSymbol,下面的對象說明的這一點:數據結構

const symbol = Symbol();
const string2 = 'string2';

const regularObject = {
  string1: 'value1',
  [string2]: 'value2',
  [symbol]: 'value3'
};
複製代碼

相比之下,Map容許你使用函數、對象和其它簡單的類型(包括NaN)做爲鍵,以下代碼:函數

const func = () => null;
const object = {};
const array = [];
const bool = false;
const map = new Map();

map.set(func, 'value1');
map.set(object, 'value2');
map.set(array, 'value3');
map.set(bool, 'value4');
map.set(NaN, 'value5');
複製代碼

在連接不一樣數據類型時,這個特性提供了極大的靈活性。性能

2.直接遍歷

在常規對象中,爲了遍歷keys、values和entries,你必須將它們轉換爲數組,如使用Object.keys()Object.values()Object.entries(),或者使用for ... in循環,由於常規對象不能直接遍歷,另外for ... in循環還有一些限制:它僅僅遍歷可枚舉屬性、非Symbol屬性,而且遍歷的順序是任意的。測試

Map能夠直接遍歷,而且因爲它是鍵控集合,遍歷的順序和插入鍵值的順序是一致的。你可使用for ... of循環或forEach方法來遍歷Map的entries,以下代碼:ui

for (let [key, value] of map) {
  console.log(key);
  console.log(value);
};
map.forEach((key, value) => {
  console.log(key);
  console.log(value);
});
複製代碼

還有一個好處就是,你能夠調用map.size屬性來獲取鍵值數量,而對於常規對象,爲了作到這樣你必須先轉換爲數組,而後獲取數組長度,如:Object.keys({}).lengthspa

MapSet有何不一樣

Map的行爲和Set很是類似,而且它們都包含一些相同的方法,包括:has、get、set、delete。它們二者都是鍵控集合,就是說你可使用像forEach的方法來遍歷元素,順序是按照插入鍵值排列的。code

最大的不一樣是Map經過鍵值(key/value)成對出現,就像你能夠把一個數組轉換爲Set,你也能夠把二維數組轉換爲Map

const set = new Set([1, 2, 3, 4]);
const map = new Map([['one', 1], ['two', 2], ['three', 3], ['four', 4]]);
複製代碼

類型轉換

要將Map切換回數組,你可使用ES6的結構語法:

const map = new Map([['one', 1], ['two', 2]]);
const arr = [...map];
複製代碼

到目前爲止,將Map與常規對象的互相轉換依然不是很方便,因此你可能須要依賴一個函數方法,以下:

const mapToObj = map => {
  const obj = {};
  map.forEach((key, value) => { obj[key] = value });
  return obj;
};
const objToMap = obj => {
  const map = new Map();
  Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
  return map;
};
複製代碼

可是如今,在八月份ES2019的首次展現中,咱們看見了Object引入了2個新方法:Object.entries()Object.fromEntries(),這可使上述方法簡化許多:

const obj2 = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));
複製代碼

在你使用Object.fromEntries轉換map爲object以前,確保map的key在轉換爲字符串時會產生惟一的結果,不然你將面臨數據丟失的風險。

性能測試

爲了準備測試,我會建立一個對象和一個map,它們都有1000000個相同的鍵值。

let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
  obj[i] = i;
  map.set(i, i);
}
複製代碼

而後我使用console.time()來衡量測試,因爲我特定的系統和Node.js版本的緣由,時間精度可能會有波動。測試結果展現了使用Map的性能收益,尤爲是添加和刪除鍵值的時。

查詢

let result;
console.time('Object');
result = obj.hasOwnProperty('999999');
console.timeEnd('Object');
// Object: 0.250ms

console.time('Map');
result = map.has(999999);
console.timeEnd('Map');
// Map: 0.095ms (2.6 times faster)
複製代碼

添加

console.time('Object');
obj[n] = n;
console.timeEnd('Object');
// Object: 0.229ms

console.time('Map');
map.set(n, n);
console.timeEnd('Map');
// Map: 0.005ms (45.8 times faster!)
複製代碼

刪除

console.time('Object');
delete obj[n];
console.timeEnd('Object');
// Object: 0.376ms

console.time('Map');
map.delete(n);
console.timeEnd('Map');
// Map: 0.012ms (31 times faster!)
複製代碼

Map在什麼狀況下更慢

在測試中,我發現一種狀況常規對象的性能更好:使用for循環去建立常規對象和map。這個結果着實使人震驚,可是沒有for循環,map添加屬性的性能賽過常規對象。

console.time('Object');
for (let i = 0; i < n; i++) {
  obj[i] = i;
}
console.timeEnd('Object');
// Object: 32.143ms

let obj = {}, map = new Map(), n = 1000000;
console.time('Map');
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd('Map');
// Map: 163.828ms (5 times slower)
複製代碼

舉個例子

最後,讓咱們看一個Map比常規對象更合適的例子,好比說咱們想寫一個函數去檢查2個字符串是否由相同的字符串隨機排序。

console.log(isAnagram('anagram', 'gramana')); // Should return true
console.log(isAnagram('anagram', 'margnna')); // Should return false
複製代碼

有許多方法能夠作到,可是這裏,map能夠幫忙咱們建立一個最簡單、最快速的解決方案:

const isAnagram = (str1, str2) => {
  if (str1.length !== str2.length) {
    return false;
  }
  const map = new Map();
  for (let char of str1) {
    const count = map.has(char) ? map.get(char) + 1 : 1;
    map.set(char, count);
  }
  for (let char of str2) {
    if (!map.has(char)) {
      return false;
    }
    const count = map.get(char) - 1;
    if (count === 0) {
      map.delete(char);
      continue;
    }
    map.set(char, count);
  }
  return map.size === 0;
};
複製代碼

在這個例子中,當涉及到動態添加和刪除鍵值,沒法提早確認數據結構(或者說鍵值的數量)時,map比object更合適。


我但願這篇文章對你有所幫助,若是你以前沒有使用過Map,不妨開闊你的眼界,衡量現代JavaScript的價值體現。

譯者注:我我的不太贊成做者的觀點,從以上的描述來看,Map更像是以空間爲代價,換取速度上的提高。那麼對於空間和速度的衡量,必然存在一個閾值。在數據量比較少時,相比與速度的提高,其犧牲的空間代價更大,此時顯然是不適合使用Map;當數據量足夠大時,此時空間的代價影響更小。因此,看開發者如何衡量二者之間的關係,選擇最優解。

原文連接

相關文章
相關標籤/搜索