JavaScript中Map與Object應(yīng)用場景
引言
在 JavaScript 中選擇 Object 和 Map 的缺失指南
在 JavaScript 中,對象很方便。它們使我們能夠輕松地將多條數(shù)據(jù)組合在一起。在 ES6 之后,我們?yōu)樵撜Z言添加了一個(gè)新功能 - Map
. 在很多方面,它似乎比Object
更強(qiáng)大,但界面有點(diǎn)笨拙。然而,大多數(shù)人在需要哈希映射時(shí)仍然會(huì)使用對象,并且只有在他們意識(shí)到鍵不能只是他們用例的字符串時(shí)才切換到使用。因此,在當(dāng)今的 JavaScript 社區(qū)中Map
仍未得到充分利用。
在這篇文章中,我將分解您應(yīng)該考慮使用Map
more 的所有原因及其性能特征與基準(zhǔn)測試。
在 JavaScript 中,Object 是一個(gè)相當(dāng)寬泛的術(shù)語。幾乎所有東西都可以是一個(gè)對象,除了兩種底部類型 -null
和undefined
. 在這篇博文中,Object 僅指普通的舊對象,由左大括號(hào){
和右大括號(hào)分隔}
。
- 用于
Object
在作者時(shí)已知的屬性/字段數(shù)量固定且有限的記錄,例如配置對象。以及一般一次性使用的任何東西。 - 用于
Map
字典或哈希映射,其中條目數(shù)量可變,更新頻繁,其鍵在作者時(shí)可能未知,例如事件發(fā)射器。 - 根據(jù)我的基準(zhǔn)測試,除非鍵是小整數(shù)字符串,否則在插入、刪除和迭代速度
Map
方面確實(shí)比Object
鍵更高效,并且它消耗的內(nèi)存比Object
相同大小的要少。
為什么 Object 缺少哈希映射
將對象用于哈希映射最明顯的缺點(diǎn)可能是對象只允許字符串和符號(hào)作為鍵。任何其他類型都將通過toString
將這些方法隱式轉(zhuǎn)換為字符串。
const foo = [] const bar = {} const obj = {[foo]: 'foo', [bar]: 'bar'} console.log(obj) // {"": 'foo', [object Object]: 'bar'}
更重要的是,將對象用于哈希映射可能會(huì)導(dǎo)致混淆和安全隱患。
不需要的繼承
在 ES6 之前,獲取哈希映射的唯一方法是創(chuàng)建一個(gè)空對象。
const hashMap = {}
但是,在創(chuàng)建后,此對象不再為空。雖然hashMap
是用一個(gè)空的對象字面量制作的,但它會(huì)自動(dòng)繼承自Object.prototype
. 這就是為什么我們可以調(diào)用像hasOwnProperty
, toString
, constructor
on這樣的方法,hashMap
即使我們從未在對象上明確定義這些方法。
由于原型繼承,我們現(xiàn)在混合了兩種類型的屬性:存在于對象本身中的屬性,即它自己的屬性,以及存在于原型鏈中的屬性,即繼承的屬性。因此,我們需要額外的檢查(例如hasOwnProperty
)來確保給定的屬性確實(shí)是用戶提供的,而不是從原型繼承的。
最重要的是,由于屬性解析機(jī)制在 JavaScript 中的工作方式 Object.prototype
,運(yùn)行時(shí)的任何更改都會(huì)在所有對象中產(chǎn)生連鎖反應(yīng)。 這為原型污染攻擊打開了大門,這對于大型 JavaScript 應(yīng)用程序來說可能是一個(gè)嚴(yán)重的安全問題。
幸運(yùn)的是,我們可以通過使用來解決這個(gè)問題Object.create(null)
,這會(huì)生成一個(gè)不繼承任何內(nèi)容的對象Object.prototype
。
名稱沖突
當(dāng)一個(gè)對象自己的屬性與其原型上的屬性發(fā)生名稱沖突時(shí),它會(huì)破壞預(yù)期并因此使您的程序崩潰。
例如,我們有一個(gè)foo
接受對象的函數(shù):
function foo(obj) { //... for(const key in obj) { if(obj.hasOwnProperty(key)) { } } }
存在可靠性風(fēng)險(xiǎn)obj.hasOwnProperty(key)
:考慮到屬性解析機(jī)制在 JavaScript 中的工作方式,如果obj
包含用戶提供的同名屬性,則會(huì)hasOwnProperty
隱藏Object.prototype.hasOwnProperty
. 結(jié)果,我們不知道在運(yùn)行時(shí)會(huì)準(zhǔn)確調(diào)用哪個(gè)方法。
可以進(jìn)行一些防御性編程來防止這種情況。例如,我們可以借用hasOwnProperty
來代替: Object.prototype
function foo(obj) { //... for(const key in obj) { if(Object.prototype.hasOwnProperty.call(obj, key)) { // ... } } }
一種更短的方法可能是在對象文字上調(diào)用該方法,{}.hasOwnProperty.call(key)
但它仍然很麻煩。這就是為什么有一個(gè)新添加的靜態(tài)方法Object.hasOwn
。
尺寸
Object
沒有提供方便的 API 來獲取大小,即屬性的數(shù)量。構(gòu)成對象大小的因素也有細(xì)微差別:
- 如果您只關(guān)心字符串、可枚舉鍵,那么您可以將鍵轉(zhuǎn)換為數(shù)組
Object.keys()
并獲取其length
. - 如果您想考慮不可枚舉的字符串鍵,那么您必須使用
Object.getOwnPropertyNames
來獲取鍵列表并獲取其長度。 - 如果您對符號(hào)鍵感興趣,可以使用
getOwnPropertySymbols
來顯示符號(hào)鍵?;蛘?,您可以使用Reflect.ownKeys
同時(shí)獲取字符串鍵和符號(hào)鍵,無論它是否可枚舉。
迭代
遍歷對象也有類似的復(fù)雜性。
我們可以使用良好的舊for...in
循環(huán)。但它揭示了繼承的可枚舉屬性:
Object.prototype.foo = 'bar' const obj = {id: 1} for(const key in obj) { console.log(key) // 'id', 'foo' }
我們不能用for...of
與對象一起使用,因?yàn)槟J(rèn)情況下它不是可迭代的,除非我們用Symbol.iterator
在其上顯式定義方法。
我們可以使用Object.keys
,Object.values
和Object.entries
來獲取可枚舉的字符串鍵(或/和值)列表,然后對其進(jìn)行迭代,這會(huì)引入額外的開銷步驟。
最后,臭名昭著的插入順序沒有得到充分尊重。在大多數(shù)瀏覽器中,整數(shù)鍵按升序排序并且優(yōu)先于字符串鍵,即使字符串鍵插入到整數(shù)鍵之前也是如此。
const obj = {} obj.foo = 'first' obj[2] = 'second' obj[1] = 'last' console.log(obj) // {1: 'last', 2: 'second', foo: 'first'}
清除
沒有簡單的方法可以從對象中刪除所有屬性,您必須使用delete
操作符一個(gè)一個(gè)地刪除每個(gè)屬性,這在歷史上被認(rèn)為是緩慢的。但是我的基準(zhǔn)測試表明,它的性能實(shí)際上比不上Map.prototype.delete
檢查屬性是否存在
最后,我們不能依賴點(diǎn)/括號(hào)符號(hào)來檢查屬性是否存在,因?yàn)橹当旧砜梢栽O(shè)置為undefined
. 相反,我們必須使用Object.prototype.hasOwnProperty
和 Object.hasOwn
。
const obj = {a: undefined} Object.hasOwn(obj, 'a') // true
哈希映射的映射
ES6 帶來了 Map。它更適合哈希映射。
首先,與Object
只允許字符串和符號(hào)作為鍵不同,它Map
支持任何數(shù)據(jù)類型的鍵。
但是,如果您Map
用于存儲(chǔ)對象的元數(shù)據(jù),那么您應(yīng)該使用它WeakMap
來避免內(nèi)存泄漏。
但更重要的是,Map
它提供了用戶定義和內(nèi)置程序數(shù)據(jù)之間的清晰分離,但代價(jià)是額外的檢索條目。
Map
還提供了更好的人體工程學(xué):Map
默認(rèn)情況下,A 是可迭代的。這意味著您可以使用for...of
輕松迭代地圖,并執(zhí)行諸如使用嵌套解構(gòu)從地圖中提取第一個(gè)條目之類的操作。
const [[firstKey, firstValue]] = map
與Object
相比Map
為各種常見任務(wù)提供專用 API:
Map.prototype.has
檢查給定條目的存在,Object.prototype.hasOwnProperty
/Object.hasOwn
在對象上相比不那么尷尬
Map.prototype.get
返回與提供的鍵關(guān)聯(lián)的值。人們可能會(huì)覺得這比對象上的點(diǎn)表示法或括號(hào)表示法更笨拙。然而,它在用戶數(shù)據(jù)和內(nèi)置方法之間提供了清晰的分離。
Map.prototype.size
返回 a 中的條目數(shù),Map
它顯然是獲得對象大小所必須執(zhí)行的操作的贏家。此外,它要快得多。
Map.prototype.clear
刪除 a 中的所有條目,Map
它比運(yùn)算符delete
快得多。
性能
在大多數(shù)情況下,JavaScript 社區(qū)似乎普遍認(rèn)為Map
比Object
好. 有些人聲稱要通過Object
切換到Map
.
我磨練 Leetcode 的經(jīng)驗(yàn)似乎證實(shí)了這個(gè)信念:Leetcode 將大量數(shù)據(jù)作為測試用例提供給您的解決方案,如果您的解決方案耗時(shí)過長,它就會(huì)超時(shí)。像這樣的問題只有在你使用Object
時(shí)才會(huì)超時(shí),而不是在Map
.
但是,我相信只是說“Map
比對象更快”是簡化的。一定有一些細(xì)微差別是我想自己找出來的。所以。我構(gòu)建了一個(gè)小應(yīng)用程序來運(yùn)行一些基準(zhǔn)測試。
基準(zhǔn)測試實(shí)施細(xì)節(jié)
該應(yīng)用程序有一個(gè)表格,顯示在Object
和Map
上測量的插入、迭代和刪除速度。
插入和迭代的性能以每秒操作數(shù)來衡量。我編寫了一個(gè) util 函數(shù)measureFor
,它重復(fù)運(yùn)行目標(biāo)函數(shù),直到達(dá)到指定的最小時(shí)間閾值(即duration
UI 上的輸入字段)。它返回每秒執(zhí)行此類函數(shù)的平均次數(shù)。
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
運(yùn)算符從對象中刪除所有屬性所需的時(shí)間,并將其與 Map.prototype.delete
相同大小的 Map 的時(shí)間進(jìn)行比較。我可以使用Map.prototype.clear
,但它違背了基準(zhǔn)測試的目的,因?yàn)槲掖_信它會(huì)更快。
在這三個(gè)操作中,我更加關(guān)注插入,因?yàn)樗俏以谌粘9ぷ髦袌?zhí)行的最常見的操作。對于迭代性能,很難提出一個(gè)包羅萬象的基準(zhǔn),因?yàn)槲覀兛梢栽诮o定對象上執(zhí)行許多不同的迭代變體。這里我只測量for ... in
循環(huán)。
我在這里使用了三種類型的鍵:
- 字符串,例如
yekwl7caqejth7aawelo4
. - 整數(shù)字符串,例如
123
. - 由 生成的數(shù)字字符串
Math.random().toString()
,例如0.4024025689756525
。
所有的鍵都是隨機(jī)生成的,所以我們不會(huì)碰到 V8 實(shí)現(xiàn)的內(nèi)聯(lián)緩存。我還顯式地將整數(shù)和數(shù)字鍵轉(zhuǎn)換為字符串,然后再將它們添加到對象以避免隱式轉(zhuǎn)換的開銷。
最后,在基準(zhǔn)測試開始之前,還有一個(gè)至少 100 毫秒的預(yù)熱階段,我們反復(fù)創(chuàng)建新的對象和地圖,這些新對象和地圖會(huì)立即被丟棄。
如果你想玩,我把代碼放在Codesandbox上。
我從100個(gè)屬性/條目的Object和Map開始,一直到5000000,讓每種類型的操作持續(xù)運(yùn)行10000ms,看看它們彼此之間的表現(xiàn)如何。以下是我的發(fā)現(xiàn)……
為什么我們在條目數(shù)達(dá)到 5000000 時(shí)停止?
字符串鍵
一般來說,當(dāng)鍵是(非數(shù)字)字符串時(shí),在所有操作上都Map
優(yōu)于Object
。
但細(xì)微差別在于,當(dāng)條目數(shù)量不是很大(低于 100000)時(shí),Map
插入速度是Object
插入速度的兩倍,但隨著大小增長超過 100000,性能差距開始縮小。
我制作了一些圖表來更好地說明我的發(fā)現(xiàn)。
上圖顯示了隨著條目數(shù)量的增加(x 軸),插入率如何下降(y 軸)。但是因?yàn)?X 軸擴(kuò)展得太寬(從 100 到 1000000),所以很難分辨這兩條線之間的差距。
然后我使用對數(shù)刻度來處理數(shù)據(jù)并制作下面的圖表。
您可以清楚地看出兩條線正在匯合。
我制作了另一個(gè)圖表,繪制了Map
與Object
插入速度相關(guān)的速度。您可以看到Map
開始時(shí)比Object
快. 然后隨著時(shí)間的推移,性能差距開始縮小。Map
隨著規(guī)模增長到 5000000,最終速度僅快 30%。
但是,我們大多數(shù)人在一個(gè)對象或映射中永遠(yuǎn)不會(huì)有超過 100 萬個(gè)條目。具有數(shù)百或數(shù)千個(gè)條目的大小. 因此,我們是否應(yīng)該把它留在那兒,然后全力以赴開始重構(gòu)我們的代碼庫Map
?
絕對不會(huì)……或者至少?zèng)]有期望我們的應(yīng)用程序會(huì)快 2 倍。請記住,我們還沒有探索過其他類型的鍵。讓我們看一下整數(shù)鍵。
整數(shù)鍵
我特別想對具有整數(shù)鍵的對象運(yùn)行基準(zhǔn)測試的原因是 V8 在內(nèi)部優(yōu)化了整數(shù)索引屬性并將它們存儲(chǔ)在可以線性和連續(xù)訪問的單獨(dú)數(shù)組中。我找不到任何資源來確認(rèn)它對Map
采用了相同類型的優(yōu)化。
讓我們首先嘗試 [0, 1000] 范圍內(nèi)的整數(shù)鍵。
正如我所料,這次Object
跑贏大盤。 Object
插入速度比Map
快 65%,迭代速度快 16%。
讓我們擴(kuò)大范圍,使鍵中的最大整數(shù)為 1200。
現(xiàn)在似乎Map
開始比Object
快一點(diǎn)
現(xiàn)在我們只增加了整數(shù)鍵的范圍,而不是和的實(shí)際Object
大小Map
。讓我們增大尺寸,看看它如何影響性能。
當(dāng)屬性大小為1000時(shí),Object
的插入速度比Object
快70%,迭代速度比Map
慢2倍。
我嘗試了許多不同的Object
/Object
大小和整數(shù)鍵范圍的組合,但未能想出一個(gè)明確的模式。但我看到的總體趨勢是,隨著大小的增長,使用一些相對較小的整數(shù)作為鍵,Object
在插入方面的性能可以比map
更好,總是與刪除大致相同,迭代速度是map
的4到5倍。最大整數(shù)鍵的閾值,即Object
在插入時(shí)開始變慢的閾值,將隨著Object
的大小而增長。例如,當(dāng)該Object
只有100個(gè)表項(xiàng)時(shí),閾值為1200;當(dāng)它有10000個(gè)條目時(shí),閾值似乎在24000左右。
數(shù)字鍵
最后,我們來看看最后一種鍵——數(shù)字鍵。
從技術(shù)上講,以前的整數(shù)鍵也是數(shù)字的。這里的數(shù)字鍵特指生成的數(shù)字字符串Math.random().toString()
。
結(jié)果與字符串-鍵的情況類似:map
開始時(shí)比Object
快得多(插入和刪除快2倍,迭代快4-5倍),但隨著大小的增加,增量越來越小。
嵌套對象/地圖呢?
內(nèi)存使用情況
基準(zhǔn)測試的另一個(gè)重要方面是內(nèi)存利用率。
由于我無法控制瀏覽器環(huán)境中的垃圾收集器,因此我決定在 Node.js 中運(yùn)行基準(zhǔn)測試。
我創(chuàng)建了一個(gè)小腳本來測量它們各自的內(nèi)存使用情況,并在每次測量中手動(dòng)觸發(fā)完全垃圾收集。運(yùn)行它,node --expose-gc
我得到以下結(jié)果:
{
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
消耗的內(nèi)存比Object
少20%到50%,這并不奇怪,因?yàn)?code>Map它不存儲(chǔ)屬性描述符,例如writable
// like 。enumerable``configurable``Object
結(jié)論
那么我們能從這一切中得到什么?
-
Map
比Object
更快,除非您有小的整數(shù)和數(shù)組索引鍵,而且它的內(nèi)存效率更高。 - 如果需要經(jīng)常更新的哈希映射,可以使用
Map
; 如果你想要一個(gè)固定的鍵值集合(例如記錄),請使用Object
,并注意原型繼承帶來的陷阱。
如果您確切了解 V8 如何優(yōu)化的細(xì)節(jié),
Map
或者只是想指出我的基準(zhǔn)測試中的缺陷,請聯(lián)系我。我很樂意根據(jù)您的信息更新這篇文章!
瀏覽器兼容性注意事項(xiàng)
Map
是 ES6 的一個(gè)特性。到目前為止,我們大多數(shù)人都不應(yīng)該擔(dān)心它的兼容性,除非你的目標(biāo)用戶群是一些小眾的舊瀏覽器。“舊”是指比 IE 11 更早,因?yàn)榧词?IE 11 也支持Map而此時(shí) IE 11已死。我們不應(yīng)該在默認(rèn)情況下盲目地轉(zhuǎn)譯和添加 polyfill 到目標(biāo) ES5,因?yàn)樗粌H會(huì)膨脹你的包大小,而且與現(xiàn)代 JavaScript 相比運(yùn)行起來很慢。最重要的是,它會(huì)懲罰 99.999% 的使用現(xiàn)代瀏覽器的用戶。
另外,我們不必放棄對舊版瀏覽器的支持——nomodule
通過提供后備包來提供舊版代碼,這樣我們就可以避免使用現(xiàn)代瀏覽器降低訪問者的體驗(yàn)。
JavaScript 語言在不斷發(fā)展,平臺(tái)在優(yōu)化現(xiàn)代 JavaScript 方面也越來越好。我們不應(yīng)該以瀏覽器兼容性為借口忽略所有已做出的改進(jìn)。
原文翻譯于https://www.zhenghao.io/posts/object-vs-map
以上就是JavaScript中Map與Object應(yīng)用場景的詳細(xì)內(nèi)容,更多關(guān)于JavaScript中Map Object的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Umi4集成阿里低代碼框架lowcode-engine實(shí)現(xiàn)
這篇文章主要為大家介紹了Umi4集成阿里低代碼框架lowcode-engine實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08前端通過JavaScript創(chuàng)建修改CAD圖形詳情
這篇文章介紹JavaScript創(chuàng)建修改CAD圖形,創(chuàng)建修改CAD圖形,一般是基于AutoCAD進(jìn)行二次開發(fā),ObjectARX是AutoDesk公司針對AutoCAD平臺(tái)上的二次開發(fā)而推出的一個(gè)開發(fā)軟件包,它提供了以C++為基礎(chǔ)的面向?qū)ο蟮拈_發(fā)環(huán)境及應(yīng)用程序接口,能真正快速的訪問AutoCAD圖形數(shù)據(jù)庫2021-10-10javascript的setTimeout()使用方法總結(jié)
這篇文章主要給大家分享javascript的setTimeout()使用方法總結(jié),js的setTimeout方法用處比較多,通常用在頁面刷新了、延遲執(zhí)行了等等,下面我們一起來看看文章對該內(nèi)容的具體總結(jié)吧,需要的朋友可以參考一下2021-11-1113個(gè)JavaScript 一行程序,讓你看起來就是個(gè)專家
JavaScript 可以做很多好玩的事, 從復(fù)雜的框架到處理API,有太多的東西需要學(xué)習(xí)。但是,它也能讓我們只用一行就能做一些了不起的事情。今天的文章小編就為大家介紹13 個(gè)JavaScript 行程序,需要的朋友可以參考下2021-08-08