JavaScript 中的 Map vs Object

2022-08-29 23:05:40

我刚刚发现了这个功能

映射:映射对象是简单的键/值映射。

这让我感到困惑。常规的JavaScript对象是字典,那么Map与字典有什么不同呢?从概念上讲,它们是相同的(根据Stack Overflow上的另一个问题)

文档也没有帮助:

映射对象是键/值对的集合,其中键和值都可以是任意的 ECMAScript 语言值。不同的键值只能出现在 Map 集合中的一个键/值对中。使用创建 Map 时选择的比较算法区分的不同键值。

Map 对象可以按插入顺序循环访问其元素。映射对象必须使用哈希表或其他机制来实现,这些机制平均而言,这些机制提供的访问时间对集合中的元素数量是子线性的。此 Map 对象规范中使用的数据结构仅用于描述 Map 对象所需的可观察语义。它不是一个可行的实现模型。

...对我来说,这听起来仍然像一个对象,所以很明显我错过了一些东西。

为什么JavaScript会获得一个(得到良好支持的)Map对象?它有什么作用?


答案 1

根据MDN的说法:

Map 对象可以按插入顺序迭代其元素 - 循环将为每次迭代返回一个 [键,值] 数组。for..of

对象与 Maps 类似,因为两者都允许您将键设置为值、检索这些值、删除键以及检测某些内容是否存储在键上。因此,对象在历史上一直被用作地图;但是,对象和地图之间存在重要差异,可以更好地使用地图。

对象具有原型,因此映射中有默认键。但是,可以使用map = Object.create(null)绕过这一点。对象的键是字符串,其中它们可以是 Map 的任何值。您可以轻松获取地图的大小,同时必须手动跟踪对象的大小。

地图

按顺序迭代是开发人员长期以来一直想要的功能,部分原因是它确保了所有浏览器中的相同性能。所以对我来说,这是一个很大的问题。

myMap.has(key) 方法将特别方便,还有 myMap.size 属性。


答案 2

主要区别在于,对象仅支持字符串和符号键,而地图或多或少支持任何键类型。

如果我这样做,然后我会得到而不是.地图将保留密钥的类型并返回,这很好。地图还允许您使用对象作为键。传统上,要做到这一点,你必须给对象某种唯一的标识符来对它们进行哈希处理(我不认为我见过像JavaScript这样的东西作为标准的一部分)。地图还可以保证保存秩序,因此可以更好地保存,有时可以节省您需要进行一些排序的时间。obj[123] = trueObject.keys(obj)["123"][123][123]getObjectId

在实践中,地图和对象之间有几个优点和缺点。对象在非常紧密地集成到JavaScript的核心中,从而获得了优点和缺点,这使得它们与Map在关键支持方面的差异之外显着区分开来。

一个直接的优点是,您可以对对象提供语法支持,从而轻松访问元素。您还可以通过 JSON 直接支持它。当用作哈希时,获取没有任何属性的对象很烦人。默认情况下,如果要将对象用作哈希表,它们将被污染,并且您在访问属性时经常需要调用它们。您可以在此处查看默认情况下对象是如何被污染的,以及如何创建希望未受污染的对象以用作哈希:hasOwnProperty

({}).toString
    toString() { [native code] }
JSON.parse('{}').toString
    toString() { [native code] }
(Object.create(null)).toString
    undefined
JSON.parse('{}', (k,v) => (typeof v === 'object' && Object.setPrototypeOf(v, null) ,v)).toString
    undefined

对象上的污染不仅会使代码更烦人,更慢等,而且还可能对安全性产生潜在后果。

对象不是纯粹的哈希表,但它们正在尝试做更多的事情。你有头痛,比如,不能轻易获得长度()等等。对象并不意味着纯粹用作哈希映射,而是作为动态可扩展对象,因此当您将它们用作纯哈希表时,会出现问题。hasOwnPropertyObject.keys(obj).length

各种常见操作的比较/列表:

Object:
   var o = {};
   var o = Object.create(null);
   o.key = 1;
   o.key += 10;
   for(let k in o) o[k]++;
   var sum = 0;
   for(let v of Object.values(m)) sum += v;
   if('key' in o);
   if(o.hasOwnProperty('key'));
   delete(o.key);
   Object.keys(o).length
Map:
   var m = new Map();
   m.set('key', 1);
   m.set('key', m.get('key') + 10);
   m.foreach((k, v) => m.set(k, m.get(k) + 1));
   for(let k of m.keys()) m.set(k, m.get(k) + 1);
   var sum = 0;
   for(let v of m.values()) sum += v;
   if(m.has('key'));
   m.delete('key');
   m.size();

还有其他一些选项,方法,方法等,具有不同的起伏(性能,简洁,可移植,可扩展等)。对象作为语言的核心有点奇怪,所以你有很多静态方法来使用它们。

除了Maps保留键类型以及能够支持对象作为键之类的东西的优势之外,它们还与对象具有的副作用隔离开来。Map是一个纯粹的哈希值,尝试同时成为一个对象不会感到困惑。地图还可以使用代理功能轻松扩展。对象当前有一个代理类,但是性能和内存使用量很严峻,实际上创建自己的代理,看起来像对象的Map,目前比代理更好。

地图的一个重大缺点是 JSON 不直接支持它们。解析是可能的,但它有几个挂断:

JSON.parse(str, (k,v) => {
    if(typeof v !== 'object') return v;
    let m = new Map();
    for(k in v) m.set(k, v[k]);
    return m;
});

以上将引入严重的性能影响,并且也不会支持任何字符串键。JSON编码更加困难和成问题(这是许多方法之一):

// An alternative to this it to use a replacer in JSON.stringify.
Map.prototype.toJSON = function() {
    return JSON.stringify({
        keys: Array.from(this.keys()),
        values: Array.from(this.values())
    });
};

如果您纯粹使用Maps,这并不是那么糟糕,但是当您混合类型或使用非标量值作为键时,它会遇到问题(并不是说JSON非常适合这种问题,因为它是IE循环对象引用)。我还没有测试过它,但与Stringify相比,它可能会严重损害性能。

其他脚本语言通常没有这样的问题,因为它们具有Map,Object和Array的显式非标量类型。对于非标量类型,Web开发通常是一种痛苦,您必须处理诸如PHP将Array/Map与Object合并之类的事情,使用A / M的属性和JavaScript合并Map/Object与数组扩展M / O。

到目前为止,这些主要是围绕实现的问题,但基本操作的性能也很重要。性能也很复杂,因为它取决于发动机和使用情况。带着一粒盐来测试,因为我不能排除任何错误(我必须匆忙)。您还应该运行自己的测试来确认,因为我只检查非常具体的简单场景,以仅给出粗略的指示。根据Chrome中对非常大的对象/映射的测试,对象的性能更差,因为删除显然与键的数量不成比例,而不是O(1):

Object Set Took: 146
Object Update Took: 7
Object Get Took: 4
Object Delete Took: 8239
Map Set Took: 80
Map Update Took: 51
Map Get Took: 40
Map Delete Took: 2

Chrome显然在获取和更新方面具有很强的优势,但删除性能却很糟糕。在这种情况下,地图使用的内存量(开销)略多,但是由于仅使用数百万个键测试了一个对象/映射,因此对映射的开销的影响没有得到很好的表达。使用内存管理对象,如果我正确阅读配置文件,则对象似乎也更早释放,这可能是有利于对象的一个好处。

在 Firefox 中,对于这个特定的基准测试,情况就不同了:

Object Set Took: 435
Object Update Took: 126
Object Get Took: 50
Object Delete Took: 2
Map Set Took: 63
Map Update Took: 59
Map Get Took: 33
Map Delete Took: 1

我应该立即指出,在这个特定的基准测试中,从Firefox中的对象中删除不会引起任何问题,但是在其他基准测试中,它引起了问题,特别是当有很多键时,就像在Chrome中一样。在 Firefox 中,对于大型集合,地图显然更胜一筹。

然而,这并不是故事的结局,那么许多小物体或地图呢?我已经对此进行了快速基准测试,但不是详尽的基准测试(设置/获取),在上述操作中使用少量键时性能最佳。此测试更多地涉及内存和初始化。

Map Create: 69    // new Map
Object Create: 34 // {}

同样,这些数字各不相同,但基本上Object具有良好的领先优势。在某些情况下,Objects比地图领先(约10倍),但平均而言,它比地图高出约2-3倍。似乎极端的性能峰值可以双向工作。我只在Chrome和创建中对此进行了测试,以分析内存使用情况和开销。我很惊讶地看到,在Chrome中,使用一个键的地图似乎比使用一个键的对象使用大约30倍的内存。

要使用上述所有操作(4 个键)测试许多小对象:

Chrome Object Took: 61
Chrome Map Took: 67
Firefox Object Took: 54
Firefox Map Took: 139

在内存分配方面,它们在释放/GC方面表现相同,但Map使用的内存是其五倍。这个测试使用了四个键,与上一个测试一样,我只设置了一个键,这样可以解释内存开销的减少。我运行了这个测试几次,就整体速度而言,Map/Object或多或少地与Chrome并驾齐驱。在 Firefox 中,对于小对象,总体上比地图有明显的性能优势。

当然,这不包括可能变化很大的个别选项。我不会建议用这些数字进行微观优化。从中可以得到的是,根据经验,对于非常大的键值存储,更强烈地考虑地图,对于小键值存储,请考虑对象。

除此之外,最好的策略是用这两个来实现它,并首先让它工作。在分析时,重要的是要记住,有时您在查看它们时不会认为会很慢的事情可能会非常慢,因为引擎怪癖,如对象键删除情况所示。