主要区别在于对象仅支持字符串和符号键,而地图或多或少支持任何键类型。
如果我这样做obj[123] = true
,那么Object.keys(obj)
我会得到["123"]
而不是[123]
. Map 将保留键的类型并返回[123]
,这很好。地图还允许您使用对象作为键。传统上,要做到这一点,您必须为对象提供某种唯一标识符来对它们进行哈希处理(我认为我从未getObjectId
在 JavaScript 中看到任何类似的东西作为标准的一部分)。地图还可以保证顺序的保存,因此在保存方面更全面,有时可以节省您需要做的一些事情。
在实践中,地图和对象之间有几个优点和缺点。对象的优点和缺点都非常紧密地集成到 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
对象上的污染不仅会使代码更烦人、更慢等,而且还会对安全性产生潜在的影响。
对象不是纯粹的哈希表,但它们正在尝试做更多的事情。您会头痛,例如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();
还有一些其他选项、方法、方法等,它们有不同的起伏(性能、简洁、便携、可扩展等)。对象作为语言的核心有点奇怪,因此您有很多静态方法可以使用它们。
除了 Maps 保留键类型的优点以及能够支持对象作为键之类的东西之外,它们还与对象具有的副作用隔离开来。Map 是一个纯粹的散列,尝试同时成为一个对象没有任何混淆。地图也可以通过代理功能轻松扩展。Object 目前有一个 Proxy 类,但是性能和内存使用情况很糟糕,事实上,创建自己的代理,看起来像 Map for Objects 目前比 Proxy 性能更好。
Maps 的一个重大缺点是它们不直接支持 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 与 Array 扩展 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 具有良好的领先优势。在某些情况下,对象优于地图的领先优势是极端的(高出约 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 使用了五倍的内存。这个测试使用了四个键,而在上一个测试中我只设置了一个键,所以这可以解释内存开销的减少。我运行了几次这个测试,就整体速度而言,地图/对象或多或少与 Chrome 并驾齐驱。在 Firefox for small Objects 中,整体上比地图有明显的性能优势。
这当然不包括可能变化很大的个别选项。我不建议对这些数字进行微观优化。您可以从中得到的是,根据经验,对于非常大的键值存储和对象对小键值存储更强烈地考虑映射。
除此之外,这两个的最佳策略是实施它并使其首先起作用。在分析时,重要的是要记住,有时在查看它们时您认为不会很慢的事情可能会非常慢,因为在对象键删除案例中看到的引擎怪癖。