首頁 > 軟體

JavaScript中Map與Object應用場景

2022-07-07 18:04:26

引言

在 JavaScript 中選擇 Object 和 Map 的缺失指南

在 JavaScript 中,物件很方便。它們使我們能夠輕鬆地將多條資料組合在一起。在 ES6 之後,我們為該語言新增了一個新功能 - Map. 在很多方面,它似乎比Object更強大,但介面有點笨拙。然而,大多數人在需要雜湊對映時仍然會使用物件,並且只有在他們意識到鍵不能只是他們用例的字串時才切換到使用。因此,在當今的 JavaScript 社群中Map仍未得到充分利用。

在這篇文章中,我將分解您應該考慮使用Mapmore 的所有原因及其效能特徵與基準測試。

在 JavaScript 中,Object 是一個相當寬泛的術語。幾乎所有東西都可以是一個物件,除了兩種底部型別 -nullundefined. 在這篇博文中,Object 僅指普通的舊物件,由左大括號{和右大括號分隔}

博士

  • 用於Object在作者時已知的屬性/欄位數量固定且有限的記錄,例如設定物件。以及一般一次性使用的任何東西。
  • 用於Map字典或雜湊對映,其中條目數量可變,更新頻繁,其鍵在作者時可能未知,例如事件發射器
  • 根據我的基準測試,除非鍵是小整數位符串,否則在插入、刪除和迭代速度Map方面確實比Object高效,並且它消耗的記憶體Object相同大小的要少。

為什麼 Object 缺少雜湊對映

將物件用於雜湊對映最明顯的缺點可能是物件只允許字串和符號作為鍵。任何其他型別都將通過toString將這些方法隱式轉換為字串。

const foo = []
const bar = {}
const obj = {[foo]: 'foo', [bar]: 'bar'}
console.log(obj) // {"": 'foo', [object Object]: 'bar'}

更重要的是,將物件用於雜湊對映可能會導致混淆和安全隱患。

不需要的繼承

在 ES6 之前,獲取雜湊對映的唯一方法是建立一個空物件。

const hashMap = {}

但是,在建立後,此物件不再為空。雖然hashMap是用一個空的物件字面量製作的,但它會自動繼承自Object.prototype. 這就是為什麼我們可以呼叫像hasOwnPropertytoStringconstructoron這樣的方法,hashMap即使我們從未在物件上明確定義這些方法。

由於原型繼承,我們現在混合了兩種型別的屬性:存在於物件本身中的屬性,即它自己的屬性,以及存在於原型鏈中的屬性,即繼承的屬性。因此,我們需要額外的檢查(例如hasOwnProperty)來確保給定的屬性確實是使用者提供的,而不是從原型繼承的。

最重要的是,由於屬性解析機制在 JavaScript 中的工作方式 Object.prototype,執行時的任何更改都會在所有物件中產生連鎖反應。 這為原型汙染攻擊開啟了大門,這對於大型 JavaScript 應用程式來說可能是一個嚴重的安全問題。

幸運的是,我們可以通過使用來解決這個問題Object.create(null),這會生成一個不繼承任何內容的物件Object.prototype

名稱衝突

當一個物件自己的屬性與其原型上的屬性發生名稱衝突時,它會破壞預期並因此使您的程式崩潰。

例如,我們有一個foo接受物件的函數:

function foo(obj) {
	//...
	for(const key in obj) {
		if(obj.hasOwnProperty(key)) {
		}
	}
}

存在可靠性風險obj.hasOwnProperty(key):考慮到屬性解析機制在 JavaScript 中的工作方式,如果obj包含使用者提供的同名屬性,則會hasOwnProperty隱藏Object.prototype.hasOwnProperty. 結果,我們不知道在執行時會準確呼叫哪個方法。

可以進行一些防禦性程式設計來防止這種情況。例如,我們可以借用hasOwnProperty來代替: Object.prototype

function foo(obj) {
	//...
	for(const key in obj) {
		if(Object.prototype.hasOwnProperty.call(obj, key)) {
			// ...
		}
	}
}

一種更短的方法可能是在物件文字上呼叫該方法,{}.hasOwnProperty.call(key)但它仍然很麻煩。這就是為什麼有一個新新增的靜態方法Object.hasOwn

尺寸

Object沒有提供方便的 API 來獲取大小,即屬性的數量。構成物件大小的因素也有細微差別:

  • 如果您只關心字串、可列舉鍵,那麼您可以將鍵轉換為陣列Object.keys()並獲取其length.
  • 如果您想考慮不可列舉的字串鍵,那麼您必須使用Object.getOwnPropertyNames來獲取鍵列表並獲取其長度。
  • 如果您對符號鍵感興趣,可以使用getOwnPropertySymbols來顯示符號鍵。或者,您可以使用Reflect.ownKeys 同時獲取字串鍵和符號鍵,無論它是否可列舉。

迭代

遍歷物件也有類似的複雜性。

我們可以使用良好的舊for...in迴圈。但它揭示了繼承的可列舉屬性:

Object.prototype.foo = 'bar'
const obj = {id: 1} 
for(const key in obj) {
	console.log(key) // 'id', 'foo'
}

我們不能用for...of與物件一起使用,因為預設情況下它不是可迭代的,除非我們用Symbol.iterator在其上顯式定義方法。

我們可以使用Object.keys,Object.valuesObject.entries來獲取可列舉的字串鍵(或/和值)列表,然後對其進行迭代,這會引入額外的開銷步驟。

最後,臭名昭著的插入順序沒有得到充分尊重。在大多數瀏覽器中,整數鍵按升序排序並且優先於字串鍵,即使字串鍵插入到整數鍵之前也是如此。

const obj = {}
obj.foo = 'first'
obj[2] = 'second'
obj[1] = 'last'
console.log(obj) // {1: 'last', 2: 'second', foo: 'first'}

清除

沒有簡單的方法可以從物件中刪除所有屬性,您必須使用delete操作符一個一個地刪除每個屬性,這在歷史上被認為是緩慢的。但是我的基準測試表明,它的效能實際上比不上Map.prototype.delete

檢查屬性是否存在

最後,我們不能依賴點/括號符號來檢查屬性是否存在,因為值本身可以設定為undefined. 相反,我們必須使用Object.prototype.hasOwnProperty和 Object.hasOwn

const obj = {a: undefined}
Object.hasOwn(obj, 'a') // true

雜湊對映的對映

ES6 帶來了 Map。它更適合雜湊對映。

首先,與Object只允許字串和符號作為鍵不同,它Map支援任何資料型別的鍵。

但是,如果您Map用於儲存物件的後設資料,那麼您應該使用它WeakMap來避免記憶體漏失。

但更重要的是,Map它提供了使用者定義和內建程式資料之間的清晰分離,但代價是額外的檢索條目。

Map還提供了更好的人體工程學:Map預設情況下,A 是可迭代的。這意味著您可以使用for...of輕鬆迭代地圖,並執行諸如使用巢狀解構從地圖中提取第一個條目之類的操作。

const [[firstKey, firstValue]] = map

Object相比Map為各種常見任務提供專用 API:

Map.prototype.has檢查給定條目的存在,Object.prototype.hasOwnProperty/Object.hasOwn在物件上相比不那麼尷尬

Map.prototype.get返回與提供的鍵關聯的值。人們可能會覺得這比物件上的點表示法或括號表示法更笨拙。然而,它在使用者資料和內建方法之間提供了清晰的分離。

Map.prototype.size返回 a 中的條目數,Map它顯然是獲得物件大小所必須執行的操作的贏家。此外,它要快得多。

Map.prototype.clear刪除 a 中的所有條目,Map它比運運算元delete快得多。

效能

在大多數情況下,JavaScript 社群似乎普遍認為MapObject好. 有些人聲稱要通過Object切換到Map.

我磨練 Leetcode 的經驗似乎證實了這個信念:Leetcode 將大量資料作為測試用例提供給您的解決方案,如果您的解決方案耗時過長,它就會超時。像這樣的問題只有在你使用Object時才會超時,而不是在Map.

但是,我相信只是說“Map比物件更快”是簡化的。一定有一些細微差別是我想自己找出來的。所以。我構建了一個小應用程式來執行一些基準測試。

基準測試實施細節

該應用程式有一個表格,顯示在ObjectMap 上測量的插入、迭代和刪除速度。

插入和迭代的效能以每秒運算元來衡量。我編寫了一個 util 函數measureFor,它重複執行目標函數,直到達到指定的最小時間閾值(即durationUI 上的輸入欄位)。它返回每秒執行此類函數的平均次數。

function measureFor(f, duration) {
  let iterations = 0;
  const now = performance.now();
  let elapsed = 0;
  while (elapsed < duration) {
    f();
    elapsed = performance.now() - now;
    iterations++;
  }
  return ((iterations / elapsed) * 1000).toFixed(4);
}

至於刪除,我只是要測量使用delete運運算元從物件中刪除所有屬性所需的時間,並將其與 Map.prototype.delete相同大小的 Map 的時間進行比較。我可以使用Map.prototype.clear,但它違背了基準測試的目的,因為我確信它會更快。

在這三個操作中,我更加關注插入,因為它往往是我在日常工作中執行的最常見的操作。對於迭代效能,很難提出一個包羅萬象的基準,因為我們可以在給定物件上執行許多不同的迭代變體。這裡我只測量for ... in迴圈。

我在這裡使用了三種型別的鍵:

  • 字串,例如yekwl7caqejth7aawelo4.
  • 整數位符串,例如123.
  • 由 生成的數位字串Math.random().toString(),例如0.4024025689756525

所有的鍵都是隨機生成的,所以我們不會碰到 V8 實現的內聯快取。我還顯式地將整數和數位鍵轉換為字串,然後再將它們新增到物件以避免隱式轉換的開銷。

最後,在基準測試開始之前,還有一個至少 100 毫秒的預熱階段,我們反覆建立新的物件和地圖,這些新物件和地圖會立即被丟棄。

如果你想玩,我把程式碼放在Codesandbox上。

我從100個屬性/條目的Object和Map開始,一直到5000000,讓每種型別的操作持續執行10000ms,看看它們彼此之間的表現如何。以下是我的發現……

為什麼我們在條目數達到 5000000 時停止?

字串鍵

一般來說,當鍵是(非數位)字串時,在所有操作上都Map優於Object

但細微差別在於,當條目數量不是很大(低於 100000)時,Map插入速度是Object插入速度的兩倍,但隨著大小增長超過 100000,效能差距開始縮小。

我製作了一些圖表來更好地說明我的發現。

上圖顯示了隨著條目數量的增加(x 軸),插入率如何下降(y 軸)。但是因為 X 軸擴充套件得太寬(從 100 到 1000000),所以很難分辨這兩條線之間的差距。

然後我使用對數刻度來處理資料並製作下面的圖表。

您可以清楚地看出兩條線正在匯合。

我製作了另一個圖表,繪製了MapObject插入速度相關的速度。您可以看到Map開始時比Object快. 然後隨著時間的推移,效能差距開始縮小。Map隨著規模增長到 5000000,最終速度僅快 30%。

但是,我們大多數人在一個物件或對映中永遠不會有超過 100 萬個條目。具有數百或數千個條目的大小.  因此,我們是否應該把它留在那兒,然後全力以赴開始重構我們的程式碼庫Map

絕對不會……或者至少沒有期望我們的應用程式會快 2 倍。請記住,我們還沒有探索過其他型別的鍵。讓我們看一下整數鍵。

整數鍵

我特別想對具有整數鍵的物件執行基準測試的原因是 V8 在內部優化了整數索引屬性並將它們儲存在可以線性和連續存取的單獨陣列中。我找不到任何資源來確認它對Map 採用了相同型別的優化。

讓我們首先嚐試 [0, 1000] 範圍內的整數鍵。

正如我所料,這次Object 跑贏大盤。  Object插入速度比Map快 65%,迭代速度快 16%。

讓我們擴大範圍,使鍵中的最大整數為 1200。

現在似乎Map開始比Object快一點

現在我們只增加了整數鍵的範圍,而不是和的實際Object大小Map。讓我們增大尺寸,看看它如何影響效能。

當屬性大小為1000時,Object的插入速度比Object快70%,迭代速度比Map慢2倍。

我嘗試了許多不同的Object/Object大小和整數鍵範圍的組合,但未能想出一個明確的模式。但我看到的總體趨勢是,隨著大小的增長,使用一些相對較小的整數作為鍵,Object在插入方面的效能可以比map更好,總是與刪除大致相同,迭代速度是map的4到5倍。最大整數鍵的閾值,即Object在插入時開始變慢的閾值,將隨著Object的大小而增長。例如,當該Object只有100個表項時,閾值為1200;當它有10000個條目時,閾值似乎在24000左右。

數位鍵

最後,我們來看看最後一種鍵——數位鍵。

從技術上講,以前的整數鍵也是數位的。這裡的數位鍵特指生成的數位字串Math.random().toString()

結果與字串-鍵的情況類似:map開始時比Object快得多(插入和刪除快2倍,迭代快4-5倍),但隨著大小的增加,增量越來越小。

巢狀物件/地圖呢?

記憶體使用情況

基準測試的另一個重要方面是記憶體利用率。

由於我無法控制瀏覽器環境中的垃圾收集器,因此我決定在 Node.js 中執行基準測試。

我建立了一個小指令碼來測量它們各自的記憶體使用情況,並在每次測量中手動觸發完全垃圾收集。執行它,node --expose-gc我得到以下結果:

{
  object: {
    'string-key': {
      '10000': 3.390625,
      '50000': 19.765625,
      '100000': 16.265625,
      '500000': 71.265625,
      '1000000': 142.015625
    },
    'numeric-key': {
      '10000': 1.65625,
      '50000': 8.265625,
      '100000': 16.765625,
      '500000': 72.265625,
      '1000000': 143.515625
    },
    'integer-key': {
      '10000': 0.25,
      '50000': 2.828125,
      '100000': 4.90625,
      '500000': 25.734375,
      '1000000': 59.203125
    }
  },
  map: {
    'string-key': {
      '10000': 1.703125,
      '50000': 6.765625,
      '100000': 14.015625,
      '500000': 61.765625,
      '1000000': 122.015625
    },
    'numeric-key': {
      '10000': 0.703125,
      '50000': 3.765625,
      '100000': 7.265625,
      '500000': 33.265625,
      '1000000': 67.015625
    },
    'integer-key': {
      '10000': 0.484375,
      '50000': 1.890625,
      '100000': 3.765625,
      '500000': 22.515625,
      '1000000': 43.515625
    }
  }
}

很明顯,Map消耗的記憶體比Object少20%到50%,這並不奇怪,因為Map它不儲存屬性描述符,例如writable// like 。enumerable``configurable``Object

結論

那麼我們能從這一切中得到什麼?

  •  MapObject更快,除非您有小的整數和陣列索引鍵,而且它的記憶體效率更高。
  • 如果需要經常更新的雜湊對映,可以使用Map; 如果你想要一個固定的鍵值集合(例如記錄),請使用Object,並注意原型繼承帶來的陷阱。

如果您確切瞭解 V8 如何優化的細節,Map或者只是想指出我的基準測試中的缺陷,請聯絡我。我很樂意根據您的資訊更新這篇文章!

瀏覽器相容性注意事項

Map是 ES6 的一個特性。到目前為止,我們大多數人都不應該擔心它的相容性,除非你的目標使用者群是一些小眾的舊瀏覽器。“舊”是指比 IE 11 更早,因為即使 IE 11 也支援Map而此時 IE 11已死。我們不應該在預設情況下盲目地轉譯和新增 polyfill 到目標 ES5,因為它不僅會膨脹你的包大小,而且與現代 JavaScript 相比執行起來很慢。最重要的是,它會懲罰 99.999% 的使用現代瀏覽器的使用者。

另外,我們不必放棄對舊版瀏覽器的支援——nomodule通過提供後備包來提供舊版程式碼,這樣我們就可以避免使用現代瀏覽器降低存取者的體驗。

JavaScript 語言在不斷髮展,平臺在優化現代 JavaScript 方面也越來越好。我們不應該以瀏覽器相容性為藉口忽略所有已做出的改進。

原文翻譯於https://www.zhenghao.io/posts/object-vs-map

以上就是JavaScript中Map與Object應用場景的詳細內容,更多關於JavaScript中Map Object的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com