我刚刚发现 this feature:
Map:Map 对象是简单的键/值映射。
这让我很困惑。常规 JavaScript 对象是字典,那么 Map
与字典有何不同?从概念上讲,它们是相同的(根据 another question on Stack Overflow)
该文档也无济于事:
Map 对象是键/值对的集合,其中键和值都可以是任意的 ECMAScript 语言值。不同的键值可能只出现在 Map 集合中的一个键/值对中。使用创建 Map 时选择的比较算法区分不同的键值。
Map 对象可以按插入顺序迭代其元素。 Map 对象必须使用哈希表或其他机制来实现,这些机制平均而言提供的访问时间与集合中的元素数量呈次线性关系。本 Map 对象规范中使用的数据结构仅用于描述 Map 对象所需的可观察语义。它不是一个可行的实现模型。
…对我来说仍然听起来像是一个对象,所以很明显我错过了一些东西。
为什么 JavaScript 获得了(得到很好支持的)Map
对象?它有什么作用?
根据 MDN:
Map 对象可以按插入顺序迭代其元素 - for..of 循环将为每次迭代返回一个 [key, value] 数组。
和
对象与 Maps 类似,都允许您将键设置为值、检索这些值、删除键以及检测是否在键中存储了某些内容。正因为如此,对象在历史上一直被用作地图;但是,Objects 和 Maps 之间存在重要的区别,它们可以更好地使用 Map。一个对象有一个原型,所以地图中有默认键。但是,这可以使用 map = Object.create(null) 绕过。 Object 的键是字符串,它们可以是 Map 的任何值。您可以轻松获取地图的大小,而您必须手动跟踪对象的大小。
有序迭代是开发人员长期以来一直想要的功能,部分原因是它确保了所有浏览器的相同性能。所以对我来说这是一个大问题。
myMap.has(key)
方法和 myMap.size
属性将特别方便。
关键区别在于对象仅支持字符串和符号键,而地图或多或少支持任何键类型。
如果我先执行 obj[123] = true
,然后执行 Object.keys(obj)
,那么我将得到 ["123"]
而不是 [123]
。 Map 会保留键的类型并返回 [123]
,这很好。地图还允许您使用对象作为键。传统上,要做到这一点,您必须为对象提供某种唯一标识符来散列它们(我认为我从未在 JavaScript 中看到像 getObjectId
这样的东西作为标准的一部分)。地图还可以保证秩序的保存,因此可以更好地保存,有时可以节省您需要做一些事情的时间。
在实践中,地图和对象之间有几个优点和缺点。对象的优点和缺点都非常紧密地集成到 JavaScript 的核心中,这使得它们与 Map 的显着区别在于关键支持方面的差异。
一个直接的优势是您拥有对 Objects 的语法支持,从而可以轻松访问元素。您还可以使用 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 使用 A/M 将 Array/Map 与 Object 合并作为属性,而 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 中,总体上比地图具有明显的性能优势。
这当然不包括可能变化很大的个别选项。我不建议对这些数字进行微优化。您可以从中得到的是,根据经验,对于非常大的键值存储和小键值存储的对象更强烈地考虑 Maps。
除此之外,这两者的最佳策略是实施它并使其首先发挥作用。在进行分析时,重要的是要记住,有时您在查看它们时不会认为会很慢的事情可能会非常慢,因为在对象键删除案例中可以看到引擎怪癖。
Map
可以将任何值作为键,但键查找的语义使用对象引用相等,而不是使用可能导致 problems 的 值语义。
Object.get
比 Map.get
快 10 倍。但在 Firefox 中,Object.get
比 Map.get
慢一点。由于我的应用程序都是关于获取键的值,因此听起来 Object
是要走的路。希望 Mozilla 人员在五年前进行速度测试时使 Object.get
比 Map.get
快。
object
的行为类似于字典,因为 JavaScript 是动态类型的,允许您随时添加或删除属性。
但是 Map()
更好,因为它:
提供 get、set、has 和 delete 方法。
接受任何类型的键,而不仅仅是字符串。
提供一个易于使用的迭代器并维护结果的顺序。
在迭代或复制期间没有出现带有原型和其他属性的边缘案例。
支持数以百万计的项目。
非常快。
如果您需要字典,请使用 Map()
。
但是,如果您只使用基于字符串的键并且需要最大的读取性能,那么对象可能是更好的选择。这是因为 JavaScript engines compile objects down to C++ classes 在后台,并且属性的访问路径比对 Map().get()
的函数调用要快得多。
这些类也被缓存,因此创建具有完全相同属性的新对象意味着引擎将重用现有的背景类。添加或删除属性会导致 the shape of the class to change and the backing class to be re-compiled,这就是为什么将对象用作具有大量添加和删除的字典非常慢,但在不更改对象的情况下读取现有键非常快。
因此,如果您有一个使用字符串键的一次性写入、读取繁重的工作负载,那么您可以使用 object
作为高性能字典,但对于其他所有内容,请使用 Map()
。
get set has delete
等功能,只是不太优雅(但也不错)。 Map
以何种方式更容易用于迭代?不确定我能同意。
到目前为止,我认为答案中没有提到以下几点,我认为它们值得一提。
地图可以更大
在 Chrome 中,我可以使用 Map
获得 16.7 万个键/值对,而使用常规对象可以获得 11.1 万个。带有 Map
的对几乎正好多出 50%。它们在崩溃之前都占用了大约 2 GB 的内存,所以我认为可能与 chrome 的内存限制有关(是的,尝试填充 2 Maps
并且在崩溃之前你只能得到 830 万对)。您可以使用此代码自己测试它(显然,单独而不是同时运行它们):
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") }
}
对象已经有一些属性/键
这个以前让我绊倒过。常规对象具有 toString
、constructor
、valueOf
、hasOwnProperty
、isPrototypeOf
和一堆其他预先存在的属性。对于大多数用例来说,这可能不是一个大问题,但它之前给我带来了问题。
地图可能会更慢:
由于 .get
函数调用开销和缺乏内部优化,Map can be considerably slower 对于某些任务来说比普通的旧 JavaScript 对象。
toString
、constructor
等预先存在的键(即您的键不太可能),我肯定会使用普通的旧对象与他们相撞)。它们更易于使用 - 例如,increment 是 obj[i] = (obj[i] || 0) + 1
,而对于 Map
,它是 map.set(i, (map.get(i) || 0) + 1)
,这仍然不算太糟糕,但它只是显示了事情是如何变得不必要的混乱。地图肯定有它们的用例,但通常一个普通的对象就可以了。
obj = Object.create(null)
而不是 obj = {}
来摆脱默认的 toString
、constructor
、(等)对象属性。
概括:
对象:一种数据结构,其中数据存储为键值对。在对象中,键必须是数字、字符串或符号。值可以是任何东西,也可以是其他对象、函数等。对象是无序的数据结构,即不记住键值对的插入顺序
ES6 Map:一种数据结构,其中数据存储为键值对。其中一个唯一的键映射到一个值。键和值都可以是任何数据类型。地图是一种可迭代的数据结构。这意味着插入的顺序会被记住,并且我们可以访问例如 for..of 循环中的元素。
主要区别:
Map 是有序且可迭代的,而对象不是有序且不可迭代的(从某种意义上说,它们没有 [Symbol.iterator] 属性。但是,您可以使用 for..in 语法对键进行迭代。)
我们可以将任何类型的数据作为 Map 键,而对象只能将数字、字符串或符号作为键。
Map 继承自 Map.prototype。这提供了各种实用功能和属性,这使得使用 Map 对象变得更加容易。
例子:
目的:
让 obj = {}; // 向对象添加属性 obj.prop1 = 1;对象 [2] = 2; // 获取对象的 nr 个属性 console.log(Object.keys(obj).length) // 删除一个属性 delete obj[2] console.log(obj)
地图:
const myMap = new Map(); const keyString = 'a string', keyObj = {}, keyFunc = function() {}; // 设置值 myMap.set(keyString, "value associated with 'a string'"); myMap.set(keyObj, '与 keyObj 关联的值'); myMap.set(keyFunc, '与 keyFunc 关联的值');控制台.log(myMap.size); // 3 // 获取值 console.log(myMap.get(keyString)); // “与‘字符串’关联的值” console.log(myMap.get(keyObj)); // "与 keyObj 关联的值" console.log(myMap.get(keyFunc)); // "与 keyFunc 关联的值" console.log(myMap.get('a string')); // "与 'a string' 关联的值" // 因为 keyString === 'a string' console.log(myMap.get({})); // 未定义,因为 keyObj !== {} console.log(myMap.get(function() {})) // 未定义,因为 keyFunc !== function () {}
x={}; for(let a of x){}
以 TypeError: x is not iterable
失败,但我们仍然可以传递对象的所有键/值
除了其他答案之外,我发现 Maps 比对象更笨拙和冗长。
obj[key] += x
// vs.
map.set(map.get(key) + x)
这很重要,因为更短的代码阅读速度更快、表达更直接且效果更好kept in the programmer's head。
另一方面:因为 set() 返回的是映射,而不是值,所以不可能链接分配。
foo = obj[key] = x; // Does what you expect
foo = map.set(key, x) // foo !== x; foo === map
调试地图也更痛苦。在下面,您实际上看不到地图中的键。您必须编写代码才能做到这一点。
https://i.stack.imgur.com/zdJmi.png
任何 IDE 都可以评估对象:
https://i.stack.imgur.com/RR4cd.png
Map
的构造函数不太舒服。 Object
有一个简单的文字符号。 Map
不仅没有文字,而且在构造函数中也不需要 Object
。要使用地图,你真的需要它。
何时使用地图而不是普通的 JavaScript 对象
纯 JavaScript Object
{ key: 'value' } 保存结构化数据。但是一个普通的 JavaScript 对象有它的局限性:
只有字符串和符号可以用作对象的键。如果我们使用任何其他东西,比如数字作为对象的键,那么在访问这些键期间,我们将看到这些键将被隐式转换为字符串,从而导致我们失去类型的一致性。常量名称= {1:'一个',2:'两个'};对象.键(名称); // ['1', '2'] 通过将 JavaScript 标识符写为对象的键名(例如,toString、构造函数等),可能会意外覆盖从原型继承的属性。另一个对象不能用作一个对象的键对象,因此不能通过将该对象写入另一个对象的键和另一个对象的值将包含额外信息来为该对象写入额外信息对象不是迭代器对象的大小不能直接确定
Maps 解决了 Objects 的这些限制,但我们必须将 Maps 视为 Objects 的补充而不是替代。基本上 Map 只是数组数组,但我们必须将该数组数组作为带有 new 关键字的参数传递给 Map 对象,否则仅对于数组数组,Map 的有用属性和方法不可用。并记住数组数组或 Map 中的键值对必须仅用逗号分隔,而不能像普通对象中那样使用冒号。
决定使用地图还是对象的三个技巧
当键在运行时之前未知时,对对象使用映射,因为如果这些键覆盖了对象的继承属性,则由用户输入或在不知不觉中形成的键会破坏使用该对象的代码,因此在这些情况下映射更安全。当所有键都是相同类型并且所有映射都是相同类型时,也要使用映射。如果需要将原始值存储为键,请使用映射。如果我们需要对单个元素进行操作,请使用对象。
使用地图的好处
1. Map 接受任何键类型并保留键的类型:
我们知道,如果对象的键不是字符串或符号,那么 JavaScript 会隐式地将其转换为字符串。相反,Map 接受任何类型的键:字符串、数字、布尔值、符号。等等,并且 Map 保留原始密钥类型。在这里,我们将使用数字作为 Map 中的键,它仍然是一个数字:
const numbersMap= new Map();
numbersMap.set(1, 'one');
numbersMap.set(2, 'two');
const keysOfMap= [...numbersMap.keys()];
console.log(keysOfMap); // [1, 2]
在 Map 中,我们甚至可以使用整个对象作为键。有时我们可能想要存储一些与对象相关的数据,而不是将这些数据附加到对象本身中,以便我们可以使用精简对象但想要存储有关对象的一些信息。在这些情况下,我们需要使用 Map,以便我们可以将 Object 作为 key,将 object 的相关数据作为 value。
const foo= {name: foo};
const bar= {name: bar};
const kindOfMap= [[foo, 'Foo related data'], [bar, 'Bar related data']];
但是这种方法的缺点是按键访问值的复杂性,因为我们必须遍历整个数组才能获得所需的值。
function getBy Key(kindOfMap, key) {
for (const [k, v] of kindOfMap) {
if(key === k) {
return v;
}
}
return undefined;
}
getByKey(kindOfMap, foo); // 'Foo related data'
我们可以通过使用适当的 Map 来解决无法直接访问该值的问题。
const foo= {name: 'foo'};
const bar= {name: 'bar'};
const myMap= new Map();
myMap.set(foo, 'Foo related data');
myMap.set(bar, 'Bar related data');
console.log(myMap.get(foo)); // 'Foo related data'
我们可以使用 WeakMap 来做到这一点,只需编写 const myMap= new WeakMap()。 Map 和 WeakMap 之间的区别在于 WeakMap 允许对键(这里是对象)进行垃圾收集,因此它可以防止内存泄漏,WeakMap 只接受对象作为键,并且 WeakMap 减少了方法集。
2. Map 对键名没有限制:
对于纯 JavaScript 对象,我们可能会意外覆盖从原型继承的属性,这可能很危险。这里我们将覆盖actor对象的toString()属性:
const actor= {
name: 'Harrison Ford',
toString: 'Actor: Harrison Ford'
};
现在让我们定义一个函数 isPlainObject() 来确定提供的参数是否是一个普通对象,这个函数使用 toString() 方法来检查它:
function isPlainObject(value) {
return value.toString() === '[object Object]';
}
isPlainObject(actor); // TypeError : value.toString is not a function
// this is because inside actor object toString property is a string instead of inherited method from prototype
Map 对键名没有任何限制。我们可以在这里使用toString、constructor等键名。虽然actorMap对象有一个名为toString的属性,但是继承自actorMap对象原型的toString()方法可以完美运行。
const actorMap= new Map();
actorMap.set('name', 'Harrison Ford');
actorMap.set('toString', 'Actor: Harrison Ford');
function isMap(value) {
return value.toString() === '[object Map]';
}
console.log(isMap(actorMap)); // true
如果我们遇到用户输入创建键的情况,那么我们必须将这些键放入 Map 而不是普通对象。这是因为用户可以选择自定义字段名称,例如 toString、构造函数等。然后,普通对象中的此类键名可能会破坏以后使用该对象的代码。所以正确的解决方案是将用户界面状态绑定到一个地图,没有办法打破地图:
const userCustomFieldsMap= new Map([['color', 'blue'], ['size', 'medium'], ['toString', 'A blue box']]);
3.地图是可迭代的:
要迭代一个普通对象的属性,我们需要 Object.entries() 或 Object.keys()。 Object.entries(plainObject) 返回从对象中提取的键值对数组,然后我们可以destructure这些键和值,并可以获得正常的键和值输出。
const colorHex= {
'white': '#FFFFFF',
'black': '#000000'
}
for(const [color, hex] of Object.entries(colorHex)) {
console.log(color, hex);
}
//
'white' '#FFFFFF'
'black' '#000000'
由于 Map 是可迭代的,这就是为什么我们不需要 entry() 方法来迭代 Map 和解构键,值数组可以直接在 Map 上完成,就像在 Map 内部一样,每个元素都以逗号分隔的键值对数组的形式存在.
const colorHexMap = new Map();
colorHexMap.set('white', '#FFFFFF');
colorHexMap.set('black', '#000000');
for(const [color, hex] of colorHexMap) {
console.log(color, hex);
}
//'white' '#FFFFFF' 'black' '#000000'
map.keys() 还返回一个对键的迭代器,而 map.values() 返回一个对值的迭代器。
4. 我们可以很容易地知道一个 Map 的大小
我们无法直接确定普通对象中的属性数量。我们需要一个像 Object.keys() 这样的辅助函数,它返回一个包含对象键的数组,然后使用长度属性我们可以获得键的数量或普通对象的大小。
const exams= {'John Rambo': '80%', 'James Bond': '60%'};
const sizeOfObj= Object.keys(exams).length;
console.log(sizeOfObj); // 2
但是在 Maps 的情况下,我们可以使用 map.size 属性直接访问 Map 的大小。
const examsMap = new Map([['John Rambo', '80%'], ['James Bond', '60%']]);
console.log(examsMap.size);
根据 Mozilla
JavaScript 中的 Object vs. Map 简单的例子。
Object- 遵循与map 相同的概念,即使用键值对存储数据。但是有一些细微的差别使得 map 在某些情况下表现更好。
Map- 是一种数据结构,有助于以对的形式存储数据。该对由一个唯一键和一个映射到该键的值组成。它有助于防止重复。
主要区别
Map 是一个对象的实例,但反之亦然。
变种地图 = 新地图(); var obj = 新对象(); console.log(obj instanceof Map); // false console.log(map instanceof Object); // 真的
在 Object 中,key-field 的数据类型被限制为整数、字符串和符号。而在 Map 中,键字段可以是任何数据类型(整数、数组、对象)
var map = new Map();//空map.set(1,'1'); map.set('一个', 1); map.set('{}', {name:'Hello, World!'}); map.set(12.3, 12.3) map.set([12],[12345]) for(let [key,value] of map.entries()) console.log(key+'---'+value)
在 Map 中,元素的原始顺序被保留。对于对象,情况并非如此。
让 obj ={ 1:'1', 'one':1, '{}': {name:'Hello world'}, 12.3:12.3, [12]:[100] } console.log(obj)
这是我记住它的简短方法:KOI
钥匙。对象键是字符串或符号。映射键也可以是数字(1 和“1”不同)、对象、NaN 等。它使用 === 来区分键,但有一个例外 NaN !== NaN 但您可以使用 NaN 作为键。命令。插入顺序被记住。所以 [...map] 或 [...map.keys()] 有一个特定的顺序。界面。对象:obj[key] 或 obj.a(在某些语言中,[] 和 []= 实际上是接口的一部分)。 Map 有 get()、set()、has()、delete() 等。请注意,您可以使用 map[123],但这是将其用作纯 JavaScript 对象。
除了可以按明确定义的顺序进行迭代以及使用任意值作为键的能力(-0
除外)之外,由于以下原因,映射可能很有用:
该规范强制地图操作平均是次线性的。任何对象的非愚蠢实现都将使用哈希表或类似的,因此属性查找平均可能是恒定的。然后对象可能比地图更快。但这不是规范所要求的。
对象可能有令人讨厌的意外行为。例如,假设您没有为新创建的对象 obj 设置任何 foo 属性,因此您希望 obj.foo 返回 undefined。但是 foo 可以是继承自 Object.prototype 的内置属性。或者您尝试使用赋值创建 obj.foo,但 Object.prototype 中的一些设置器运行而不是存储您的值。地图可以防止这类事情。好吧,除非某些脚本与 Map.prototype 混淆。并且 Object.create(null) 也可以,但是你失去了简单的对象初始化语法。
此处没有给予太多关注的地图的一个方面是查找。根据规范:
Map 对象必须使用哈希表或其他机制来实现,这些机制平均提供的访问时间与集合中的元素数量呈次线性关系。本 Map 对象规范中使用的数据结构仅用于描述 Map 对象所需的可观察语义。它不是一个可行的实现模型。
对于具有大量项目并需要项目查找的集合,这是一个巨大的性能提升。
TL;DR - 未指定对象查找,因此它可以按对象中元素数量的顺序排列,即 O(n)。 Map 查找必须使用哈希表或类似的,因此无论 Map 大小如何,Map 查找都是相同的,即 O(1)。
std::map
)之类的东西敞开了大门——次线性并不一定意味着 O(1)。同样在现实世界的引擎中,它们都可能被实现为哈希图(对于 V8,请参阅 v8.dev/blog/fast-properties 和 medium.com/@bpmxmqd/… 以了解引擎如何决定使用“慢”(字典)属性以及这对存储意味着什么(HashTable))
这两个技巧可以帮助您决定是使用地图还是使用对象:
当键在运行时之前未知,并且所有键的类型相同且所有值的类型相同时,对对象使用映射。
如果需要将原始值存储为键,请使用映射,因为对象将每个键视为字符串,无论是数字值、布尔值还是任何其他原始值。
当存在对单个元素进行操作的逻辑时,请使用对象。
map = Object.create(null)
绕过。什么是默认键?键与Object.prototype
有什么关系?new
运算符与Map
符号(即new Map
)一起使用,以创建地图对象。var a = {}
是(意思等同于)var a = Object.create(Object.prototype)
的简写