关键的区别是对象只支持字符串和符号键,而映射则支持或多或少的任何键类型。
If I do obj[123] = true and then Object.keys(obj) then I will get ["123"] rather than [123]. A Map would preserve the type of the key and return [123] which is great. Maps also allow you to use Objects as keys. Traditionally to do this you would have to give objects some kind of unique identifier to hash them (I don't think I've ever seen anything like getObjectId in JavaScript as part of the standard). Maps also guarantee preservation of order so are all round better for preservation and can sometimes save you needing to do a few sorts.
实际上,在Map和对象之间有一些优点和缺点。对象被紧密地集成到JavaScript的核心中,这使得它们与Map的区别大大超出了关键支持的差异。
An immediate advantage is that you have syntactical support for Objects making it easy to access elements. You also have direct support for it with JSON. When used as a hash it's annoying to get an object without any properties at all. By default if you want to use Objects as a hash table they will be polluted and you will often have to call hasOwnProperty on them when accessing properties. You can see here how by default Objects are polluted and how to create hopefully unpolluted objects for use as hashes:
({}).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)等等。对象不是纯粹用作哈希映射,而是用作动态可扩展对象,因此当您将它们用作纯哈希表时,就会出现问题。
各种常用操作比较/列表:
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();
还有一些其他的选项、方法、方法等等,它们有不同的起伏(性能、简洁、可移植、可扩展等)。对象作为语言的核心有点奇怪,所以你有很多静态方法来处理它们。
Besides the advantage of Maps preserving key types as well as being able to support things like objects as keys they are isolated from the side effects that objects much have. A Map is a pure hash, there's no confusion about trying to be an object at the same time. Maps can also be easily extended with proxy functions. Object's currently have a Proxy class however performance and memory usage is grim, in fact creating your own proxy that looks like Map for Objects currently performs better than Proxy.
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())
});
};
如果你纯粹使用map,这还不是很糟糕,但是当你混合类型或使用非标量值作为键时就会出现问题(并不是说JSON是完美的,因为它是IE循环对象引用)。我还没有对它进行测试,但与stringify相比,它可能会严重损害性能。
其他脚本语言通常不会有这样的问题,因为它们为Map、Object和Array提供了显式的非标量类型。Web开发通常是非标量类型的痛苦,你必须处理一些事情,比如PHP使用a /M将数组/Map与对象合并为属性,JavaScript将Map/Object与数组合并为扩展M/O。合并复杂类型是高级脚本语言的魔鬼祸害。
So far these are largely issues around implementation, but performance for basic operations is important as well. Performance is also complex because it depends on engine and usage. Take my tests with a grain of salt as I cannot rule out any mistake (I have to rush this). You should also run your own tests to confirm as mine examine only very specific simple scenarios to give a rough indication only. According to tests in Chrome for very large objects/maps the performance for objects is worse because of delete which is apparently somehow proportionate to the number of keys rather than 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显然在获取和更新方面有很强的优势,但删除性能非常糟糕。在这种情况下,映射使用了少量的内存(开销),但是只有一个对象/Map要测试数百万个键,映射开销的影响没有很好地表达出来。如果我正确阅读配置文件,内存管理对象似乎也更早释放,这可能是有利于对象的一个好处。
在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有一个很好的领先。在某些情况下,物体比地图的优势是极端的(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使用了5倍多的内存。这个测试使用了四个键,而在上次测试中,我只设置了一个键,所以这可以解释内存开销的减少。我运行了几次这个测试,Map/Object在整体速度方面对Chrome来说或多或少是不分上下的。在用于小对象的Firefox中,总体上比地图有明显的性能优势。
当然,这还不包括个体选择,因为个体选择可能会有很大的差异。我不建议使用这些数据进行微优化。从中可以得到的经验是,对于非常大的键值存储,更强烈地考虑map,而对于小的键值存储,更强烈地考虑对象。
除此之外,这两者的最佳策略是先实现它,然后让它工作。在分析时,重要的是要记住,有时你认为不会慢的东西,当看到它们时,可能会非常慢,因为引擎的怪叫,如对象键删除情况所见。