在JavaScript中映射vs对象

我刚刚发现了chromestatus.com,在我失去了几个小时之后,发现了这个特性 :

地图:地图对象是简单的键/值地图。

这使我困惑。 普通的JavaScript对象是字典,那么Map与字典有什么不同呢? 从概念上讲,它们是相同的(根据地图和字典之间的区别是什么? )

文档chromestatus引用不帮助:

Map对象是键/值对的集合,其中键和值可以是任意的ECMAScript语言值。 独特的键值可能只发生在Map集合中的一个键/值对中。 使用映射创build时select的比较algorithm进行区分的关键值不同。

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

…仍然听起来像是一个对象,很显然我错过了一些东西。

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

根据mozilla:

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

对象与地图类似,都可以让您将键设置为值,检索这些值,删除键,并检测某个键是否存储了某些内容。 正因为如此,对象历史上一直被用作地图; 然而,对象和地图之间的重要区别使得使用Map更好。

一个对象有一个原型,所以在地图上有默认的键。 但是,这可以绕过使用map = Object.create(null)。 对象的键是string,它们可以是Map的任何值。 您可以轻松获取地图的大小,而您必须手动跟踪对象的大小。

当键未知时,使用映射到对象上,直到运行时,以及所有键都是相同types,并且所有值都是相同types。

当存在对单个元素进行操作的逻辑时使用对象。

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map

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

myMap.has(key)方法特别方便,也是myMap.size属性。

关键的区别是对象只支持string键,因为地图支持或多或less的任何键types。

如果我obj [123] = true,然后Object.keys(obj),那么我会得到[“123”],而不是[123]。 一个地图将保存该键的types,并返回[123]这是伟大的。 地图还允许您使用对象作为键。 传统上这样做,你将不得不给对象一些独特的标识符来散列它们(我不认为我曾经见过任何像JS中的getObjectId作为标准的一部分)。 地图也保证了秩序的保存,所以保存起来更好,有时可以节省你需要做的几种。

在地图和对象之间实际上有很多优点和缺点。 对象的优点和缺点都非常紧密地集成到JS的核心中,这使得它们远远超出了关键支持的差异。

一个直接的好处是你有对对象的语法支持,使访问元素变得很容易。 你也可以用JSON直接支持它。 当作为一个散列使用时,很烦人的是得到一个没有任何属性的对象。 默认情况下,如果你想使用对象作为散列表,他们将被污染,你将经常需要调用hasOwnProperty在他们访问属性。 你可以在这里看到如何默认对象是污染,以及如何创build希望未受污染的对象用作哈希值:

 ({}).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 

对象的污染不仅使代码更烦人,更慢等,而且还会对安全产生潜在的影响。

对象不是纯哈希表,但正试图做更多。 你有像hasOwnProperty头痛,不能容易得到长度(Object.keys(obj).length)等等。 对象不是纯粹用作散列映射,而是用作dynamic可扩展对象,所以当你把它们用作纯粹的散列表时,会出现问题。

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

  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(); 

还有一些其他的select,接近,方法等不同起起伏伏(性能,简洁,便携,可扩展等)。 对象有点奇怪是语言的核心,所以你有很多静态方法来处理它们。

除了保存键types的地图的优点以及能够支持诸如对象之类的东西之外,它们与对象所具有的副作用是分离的。 一个Map是一个纯粹的散列,在试图成为一个对象的同时也没有混淆。 地图也可以通过代理function轻松扩展。 对象目前有一个代理类,但性能和内存使用情况是严峻的,实际上创build自己的代理,看起来像地图对象目前执行比代理更好。

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

 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; }); 

上面将会介绍一个严重的性能问题,也不会支持任何string键。 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,这并不是那么糟糕,但是在混合types或者使用非标量值作为关键字(而不是JSON对于这种问题是完美的,因为它是IE圆形对象引用)时会遇到问题。 我还没有testing过,但是和stringify相比,它可能会严重损害性能。

其他脚本语言通常不存在这样的问题,因为它们具有Map,Object和Array的显式非标量types。 Web开发通常是一个非标量types的痛苦,你必须处理的事情,如PHP合并使用A / M的Array / Map与属性和JS合并Map / Object与数组扩展M / O。 合并复杂types是高级脚本语言的魔鬼。

到目前为止,这些主要是围绕着实施的问题,但是基本操作的性能也是重要的。 性能也很复杂,因为它取决于引擎和使用情况。 因为我不能排除任何错误(我必须冲过去),因此请考虑一下我的testing。 你也应该运行自己的testing来确认,因为我只能检查非常特定的简单场景,只给出一个粗略的指示。 根据Chrome浏览器对非常大的对象/地图的testing,对象的性能更糟,因为删除显然与键的数量成正比,而不是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显然在获取和更新方面有很强的优势,但是删除性能是可怕的。 在这种情况下,地图使用了更多的内存(开销),但是只有一个对象/地图用数百万个密钥进行testing,地图开销的影响不能很好地expression出来。 有了内存pipe理对象,如果我正确地读取configuration文件,也可以提前释放,这可能是有利于对象的一个​​好处。

在FireFox中,这个特定的基准testing是一个不同的故事:

 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 

我应该立即指出,在这个特定的基准testing中,从FireFox中的对象中删除并不会造成任何问题,但是在其他基准testing中,它造成了一些问题,特别是在Chrome中有很多键时。 FireFox中的地图对于大型collections显然是优越的。

然而,这不是故事的结局,那么许多小物件或地图呢? 我已经做了一个快速的基准testing,但不是一个详尽的(设置/获得),其中在上述操作中使用less量的键performance最好。 这个testing更多的是关于内存和初始化。

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

这些数字也是不同的,但基本上Object有很好的领先优势。 在某些情况下,地图上物体的引导是极端的(约好10倍),但是平均来说,它大约要好2-3倍。 看起来极端的性能峰值可以同时工作。 我只在Chrome和创build中testing了这一点,以分析内存使用情况和开销。 我很惊讶地看到,在Chrome中看起来,使用一个按键的地图比使用一个按键的对象使用大约多30倍的内存。

用以上所有操作(4个键)来testing许多小物件:

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

在内存分配方面,这些在释放/ GC方面performance相同,但是Map使用了5倍以上的内存。 这个testing使用了4个键,在最后的testing中我只设置了一个键,所以这可以解释内存开销的减less。 几次运行这个testing,Map / Object在整体速度方面总体上是和Chrome的整体比较。 在FireFox中,对于小型对象,整体而言,地图具有明确的性能优势。

这当然不包括可能会有很大差异的个别选项。 我不会build议这些数字的微观优化。 你可以从中得到的是,作为一个经验法则,对于非常大的关键值存储和小型关键值存储的对象,更加强烈地考虑地图。

除了这两个最好的战略来实现它,只是使其工作第一。 在分析时务必要记住,有时候看着它们时你不会觉得缓慢的事情会因为引擎怪癖而变得非常慢,就像对象键删除的情况一样。

到目前为止,我不认为以下几点已经被提及,我认为他们值得一提。


地图可以更大

在Chrome中,我可以通过Map获得1670万个键值对,而对于1110万的对象则是一个常规对象。 与Map几乎完全相同的50%。 在崩溃之前,它们都占用了大约2GB的内存,所以我认为可能是由于内存限制( 编辑 :是的,尝试填充2个Maps ,每个崩溃之前你只能达到830万对)。 你可以用这个代码自己testing它(分别运行它们,而不是在同一时间,显然):

 var m = new Map(); var i = 0; while(1) { m.set(((10**30)*Math.random()).toString(36), ((10**30)*Math.random()).toString(36)); i++; if(i%1000 === 0) { console.log(i/1000,"thousand") } } // versus: var m = {}; var i = 0; while(1) { m[((10**30)*Math.random()).toString(36)] = ((10**30)*Math.random()).toString(36); i++; if(i%1000 === 0) { console.log(i/1000,"thousand") } } 

对象已经有一些属性/键

这一个绊倒了我之前。 常规对象有toStringconstructorvalueOfhasOwnPropertyisPrototypeOf和一堆其他预先存在的属性。 对于大多数用例来说,这可能不是一个大问题,但是之前给我带来了一些问题。

地图可能会变慢:

由于.get函数的调用开销和缺乏内部优化,对于某些任务,Map 可能会比原来的JavaScript对象慢得多。

除了以一个明确定义的顺序进行迭代以及使用任意值作为关键字(除了-0之外)的能力之外,由于以下原因,映射可以是有用的:

  • 规范强制地图操作平均为次线。

    对象的任何非愚蠢的实现将使用一个哈希表或类似的,所以属性查找可能会平均不变。 然后对象可能比地图更快。 但这不是规范所要求的。

  • 对象可能有令人讨厌的意外行为。

    例如,假设您没有将任何foo属性设置为新创build的对象obj ,所以您希望obj.foo返回undefined。 但是foo可以是从Object.prototypeinheritance的内置属性。 或者您尝试使用赋值来创buildobj.foo ,但是Object.prototype某个setter运行而不是存储您的值。

    地图防止这种事情。 那么,除非一些脚本混乱与Map.prototype 。 而Object.create(null)也可以,但是你会失去简单的对象初始值设定语法。

这两个提示可以帮助您决定是使用Map还是Object:

  • 当键未知时,使用映射到对象上,直到运行时,以及所有键都是相同types,并且所有值都是相同types。

  • 如果需要将原始值存储为键,则使用映射,因为对象将每个键作为string或数字值,布尔值或任何其他原始值处理。

  • 当存在对单个元素进行操作的逻辑时使用对象。

来源: https : //developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Keyed_Collections#Object_and_Map_compared

除了其他的答案,我发现地图比对象更难处理和冗长。

 obj[key] += x // vs. map.set(map.get(key) + x) 

这一点很重要,因为较短的代码更快速的阅读,更直接的expression,更好地保存在程序员的脑海中 。

另一方面:因为set()返回的是地图,而不是值,所以不可能连锁分配。

 foo = obj[key] = x; // Does what you expect foo = map.set(key, x) // foo !== x; foo === map 

debugging地图也更加痛苦。 下面,你实际上看不到地图上的按键。 你必须编写代码来做到这一点。

祝你好运评估一个Map Iterator

对象可以由任何IDE进行评估:

WebStorm评估一个对象