我刚刚发现了chromestatus.com,在我失去了几个小时之后,找到了this feature entry:
地图:地图对象是简单的键/值映射。
让我感到困惑。常规JavaScript对象是字典,因此Map
与字典有什么不同?从概念上讲,它们是相同的(根据What is the difference between a Map and a Dictionary?)
文档chromestatus引用也无济于事:
映射对象是键/值对的集合,其中键和值都可以是任意ECMAScript语言值。不同的键值只能出现在Map集合中的一个键/值对中。使用在创建Map时选择的比较算法区分不同的键值。
Map对象可以按插入顺序迭代其元素。必须使用散列表或其他机制来实现Map对象,这些机制平均提供对集合中元素数量的次线性访问时间。此Map对象规范中使用的数据结构仅用于描述Map对象所需的可观察语义。它并不是一个可行的实施模型。
...对我来说仍然听起来像是一个对象,显然我已经错过了一些东西。
为什么JavaScript获得(受到良好支持的)Map
对象?它做了什么?
答案 0 :(得分:208)
根据mozilla的说法:
Map对象可以按插入顺序迭代其元素 - for..of循环将为每次迭代返回[key,value]数组。
和
对象类似于Maps,因为它们都允许您将键设置为值, 检索这些值,删除键,并检测是否有 存储在钥匙上。因此,Objects已被用作Maps 历史;但是,对象之间存在重要差异 以及更好地使用地图的地图。
对象有一个原型,因此地图中有默认键。 但是,可以使用map = Object.create(null)绕过此值。该 对象的键是字符串,它们可以是Map的任何值。 您必须手动保留,才能轻松获得地图的大小 跟踪对象的大小。
在密钥未知时直到运行时和何时使用映射到对象上 所有键都是相同的类型,所有值都是相同的类型。
当存在对各个元素进行操作的逻辑时使用对象。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map
可迭代顺序是开发人员长期以来一直需要的功能,部分原因是它确保了所有浏览器的相同性能。所以对我来说这是一个很大的问题。
myMap.has(key)
方法特别方便,还有myMap.size
属性。
答案 1 :(得分:80)
关键区别在于Objects只支持字符串键,而Maps支持或多或少的任何键类型。
如果我执行obj [123] = true然后是Object.keys(obj),那么我将[" 123"]而不是[123]。 Map会保留键的类型并返回[123],这很棒。地图还允许您使用对象作为键。传统上要做到这一点,你必须给对象一些独特的标识符来哈希它们(我不认为我曾经在JS中看到像getObjectId这样的东西作为标准的一部分)。地图还保证了订单的保存,因此更好地保存,有时可以节省您需要做的一些。
在地图和实物之间的实践中,有几个优点和缺点。对象获得的优点和缺点都非常紧密地集成到JS的核心中,这使得它们远远超出了重要支持的差异。
一个直接的优势是您可以对Object进行语法支持,从而轻松访问元素。您还可以使用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();
还有一些其他的选择,接近,方法等,具有不同的起伏(性能,简洁,便携,可扩展等)。对象有点奇怪是语言的核心,所以你有很多静态的方法来处理它们。
除了地图保留键类型的优势以及能够支持对象之类的键作为键之外,它们还与对象所具有的副作用隔离开来。 Map是一个纯粹的哈希,对于尝试同时成为一个对象没有任何困惑。也可以使用代理功能轻松扩展地图。对象目前有一个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())
});
};
如果您纯粹使用地图但是在混合类型或使用非标量值作为键时会出现问题并不是那么糟糕(并非JSON完全适合这类问题,IE圆形物体参考)。我还没有对它进行过测试,但与stringify相比,它可能会严重损害性能。
其他脚本语言通常没有这样的问题,因为它们具有明确的地图,对象和数组的非标量类型。 Web开发通常是非标量类型的痛苦,你必须处理诸如PHP将Array / Map与Object合并使用A / M作为属性以及JS合并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使用的内存增加了5倍。这个测试使用了4个键,就像在上一个测试中我只设置了一个键,这样可以解释内存开销的减少。我对这个测试进行了几次测试,并且就整体速度而言,Map / Object在整体上对于Chrome来说或多或少都是颈部和颈部。在用于小型对象的FireFox中,与整体地图相比,它具有明显的性能优势。
这当然不包括可能变化很大的个别选项。我不建议用这些数字进行微观优化。您可以从中获得的是,根据经验,对于非常大的键值存储和小键值存储的对象,请考虑更强的映射。
除此之外,使用这两者的最佳策略是实现它并让它首先工作。在进行分析时,重要的是要记住,有时候看到它们时你不会想到的东西会很慢,因为对象密钥删除情况会引起引擎怪癖。
答案 2 :(得分:17)
到目前为止,我认为答案中没有提到以下几点,我认为值得一提。
在Chrome中,我可以使用Map
获得 16.7 百万个键/值对,使用常规对象获得 11.1 百万。与Map
几乎完全相同的50%。它们在崩溃之前都占用了大约2GB的内存,所以我认为可能与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
函数调用开销和缺乏内部优化,因此映射can be considerably slower而不是普通的旧JavaScript对象用于某些任务。
答案 3 :(得分:6)
除了其他答案之外,我发现地图比对象操作更笨拙和冗长。
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
调试地图也比较痛苦。下面,您无法真正查看地图中的键。你必须编写代码才能做到这一点。
任何IDE都可以评估对象:
答案 4 :(得分:6)
何时使用Google地图代替普通的JavaScript对象?
普通的JavaScript对象{key:'value'}保存结构化数据。但是普通的JS对象有其局限性:
仅字符串和符号可用作对象的键。如果我们使用其他任何说法,将数字作为对象的键,则在访问这些键期间,我们将看到这些键将隐式转换为字符串,从而使我们失去类型的一致性。 const names = {1:“一个”,2:“两个”}; Object.keys(名称); // ['1','2']
通过将JS标识符写为对象的键名(例如toString,构造函数等),有可能会意外覆盖原型的继承属性。
另一个对象不能用作对象的键,因此不能通过将该对象写为另一个对象的键来为该对象写入额外的信息,并且另一个对象的值将包含该额外的信息
对象不是迭代器
无法直接确定对象的大小
对象的这些限制已由地图解决,但我们必须将地图视为对象的补充,而不是替代。基本上,Map只是数组数组,但我们必须将该数组数组作为带有new关键字的参数传递给Map对象,否则,仅对于数组数组,Map的有用属性和方法不可用。请记住,数组数组或Map中的键/值对必须仅用逗号分隔,没有像普通对象中那样的冒号。
确定使用地图还是对象的3条提示:
在直到运行时才知道键的情况下在对象上使用映射,因为如果这些键覆盖了对象的继承属性,则由用户输入或在不知不觉中形成的键会破坏使用该对象的代码,因此在这些对象中使用map更为安全案件。当所有键为相同类型且所有图为相同类型时,也请使用图。
如果需要将原始值存储为键,请使用地图。
如果需要对单个元素进行操作,请使用对象。
使用地图的好处是:
1。 Map接受任何密钥类型并保留密钥类型:
我们知道,如果对象的键不是字符串或符号,则JS会将其隐式转换为字符串。相反,Map接受任何类型的键:字符串,数字,布尔值,符号等。Map保留原始键类型。在这里,我们将数字用作地图中的键,它将保留为数字:
const numbersMap= new Map();
numbersMap.set(1, 'one');
numbersMap.set(2, 'two');
const keysOfMap= [...numbersMap.keys()];
console.log(keysOfMap); // [1, 2]
在地图内部,我们甚至可以使用整个对象作为键。有时候,我们可能想存储一些与对象相关的数据,而不是将这些数据附加到对象本身内部,以便我们可以使用精益对象,但希望存储有关该对象的某些信息。在这些情况下,我们需要使用Map,以便我们可以将Object作为键,并将对象的相关数据作为值。
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对键名没有限制:
对于普通的JS对象,我们可能会意外覆盖从原型继承的属性,这很危险。在这里,我们将覆盖actor对象的toString()属性:
const actor= {
name: 'Harrison Ford',
toString: 'Actor: Harrison Ford'
};
现在让我们定义一个fn isPlainObject()来确定所提供的参数是否为普通对象,并且此fn使用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,构造函数等。在这里,尽管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)返回从对象中提取的一组键值对,然后我们可以对这些键和值进行解构,并获得普通键和值的输出。
const colorHex= {
'white': '#FFFFFF',
'black': '#000000'
}
for(const [color, hex] of Object.entries(colorHex)) {
console.log(color, hex);
}
//
'white' '#FFFFFF'
'black' '#000000'
由于地图是可迭代的,这就是为什么我们不需要entry()方法来迭代地图和键的解构的原因,值数组可以直接在地图上完成,就像在地图内部一样,每个元素都作为键值对数组存在被逗号隔开。
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。我们可以轻松知道地图的大小
我们无法直接确定普通对象中的属性数量。我们需要一个类似于Object.keys()的辅助函数fn,它返回一个包含对象键的数组,然后使用length属性,我们可以获得键的数量或普通对象的大小。
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);
答案 5 :(得分:3)
除了以明确定义的顺序进行迭代,以及使用任意值作为键(ProjectName
ServiceName01
01Smoke
02DataVariance
ServiceName02
01Smoke
02DataVariance
..
ServiceNameNN
01Smoke
02DataVariance
除外)的能力之外,由于以下原因,地图可能很有用:
规范强制执行地图操作平均为次线性。
对象的任何非愚蠢的实现都将使用哈希表或类似的,因此属性查找可能会平均保持不变。然后对象可能比地图更快。但规范并不要求这样做。
对象可能会有令人讨厌的意外行为。
例如,假设您没有为新创建的对象-0
设置任何foo
属性,因此您希望obj
返回undefined。但obj.foo
可以是从foo
继承的内置属性。或者您尝试使用作业创建Object.prototype
,但obj.foo
中的某个setter会运行而不是存储您的值。
地图会阻止这类事情发生。好吧,除非某些脚本与Object.prototype
混淆。 Map.prototype
也会起作用,但是你会失去简单的对象初始化语法。
答案 6 :(得分:3)
对象的行为可以像字典一样,因为Javascript是动态类型的,但它们实际上并非如此。
新的Map()
功能更好,因为它具有正常的get/set/has/delete
方法,接受键的任何类型而不仅仅是字符串,在迭代时更容易使用,并且它没有显示原型和其他属性的边缘情况。它也非常快,随着引擎变得越来越快而越来越快。 99%的时间你应该使用Map()
。
但是,如果您只使用基于字符串的键并且需要最大的读取性能,那么对象可能是更好的选择。细节是(几乎所有)javascript引擎在后台将对象编译为C ++类。这些类型由其" outline"进行缓存和重用,因此当您创建具有相同确切属性的新对象时,引擎将重用现有的背景类。这些类的属性的访问路径非常优化,并且比查找Map()
快得多。
添加或删除属性会导致重新编译缓存的支持类,这就是为什么将对象用作具有大量键添加和删除的字典的原因非常慢,但是在不更改对象的情况下读取和分配现有键是非常快。
因此,如果您使用字符串键进行一次性读取繁重工作负载,则使用object
作为专用高性能字典,但对于其他所有字典,请使用Map()
。< /强>
答案 7 :(得分:2)
Object
:一种数据结构,其中数据作为键值对存储。在对象中,键必须是数字,字符串或符号。该值可以是任何东西,其他对象,函数等也可以。对象是无序数据结构,即不记得插入键值对的顺序ES6 Map
:一种数据结构,其中数据作为键值对存储。 唯一键映射到一个值。键和值都可以为任何数据类型。映射是一个可迭代的数据结构,这意味着插入顺序会被记住,并且我们可以访问例如for..of
循环主要区别:
Map
是有序且可迭代的,而对象是无序且不可迭代的
我们可以将任何类型的数据用作Map
键,而对象只能将数字,字符串或符号用作键。
Map
继承自Map.prototype
。这提供了各种实用程序功能和属性,使使用Map
对象更加容易。
对象:
let obj = {};
// adding properties to a object
obj.prop1 = 1;
obj[2] = 2;
// getting nr of properties of the object
console.log(Object.keys(obj).length)
// deleting a property
delete obj[2]
console.log(obj)
地图:
const myMap = new Map();
const keyString = 'a string',
keyObj = {},
keyFunc = function() {};
// setting the values
myMap.set(keyString, "value associated with 'a string'");
myMap.set(keyObj, 'value associated with keyObj');
myMap.set(keyFunc, 'value associated with keyFunc');
console.log(myMap.size); // 3
// getting the values
console.log(myMap.get(keyString)); // "value associated with 'a string'"
console.log(myMap.get(keyObj)); // "value associated with keyObj"
console.log(myMap.get(keyFunc)); // "value associated with keyFunc"
console.log(myMap.get('a string')); // "value associated with 'a string'"
// because keyString === 'a string'
console.log(myMap.get({})); // undefined, because keyObj !== {}
console.log(myMap.get(function() {})) // undefined, because keyFunc !== function () {}
答案 8 :(得分:2)
我遇到了Minko Gechev的this post
,清楚地解释了主要差异。
答案 9 :(得分:1)
查找地图是地图的一个方面,在这里没有给予太多的关注。根据规格:
必须使用哈希表或其他实现Map对象 平均而言,提供次线性访问时间的机制 集合中元素的数量。使用的数据结构 本Map对象规范中的内容仅用于描述 必需的Map对象的可观察语义。并非旨在 一个可行的实施模型。
对于具有大量项目并且需要项目查找的集合,这将极大地提高性能。
TL; DR-未指定对象查找,因此它可以按对象中元素数的顺序排列,即O(n)。地图查找必须使用哈希表或类似的表,因此无论地图大小(即O(1))如何,地图查找都是相同的。
答案 10 :(得分:0)
这两个提示可以帮助您决定是使用地图还是对象:
当密钥未知时直到运行时和何时使用映射到对象上 所有键都是相同的类型,所有值都是相同的类型。
如果需要将原始值存储为键,请使用映射 因为object将每个键视为一个字符串或者是一个数字值, 布尔值或任何其他原始值。
当存在对各个元素进行操作的逻辑时使用对象。
答案 11 :(得分:0)
这是我记住它的一种简短方法:KOI
NaN
等。它使用===
来区分键,只有一个例外NaN !== NaN
,但您可以使用NaN
作为密钥。[...map]
或[...map.keys()]
具有特定的顺序。obj[key]
或obj.a
(在某种语言中,[]
和[]=
实际上是界面的一部分)。 Map有get()
,set()
,has()
,delete()
等。请注意,您可以使用map[123]
,但会将其用作普通的JS对象。答案 12 :(得分:0)
根据Mozilla
JavaScript中的对象与地图,并附有示例。
对象-遵循与映射相同的概念,即使用键值对存储数据。但是有些细微的差异使地图在某些情况下表现更好。
地图-是一种数据结构,有助于存储成对形式的数据。该对包括唯一键和映射到该键的值。它有助于防止重复。
关键区别
var map = new Map();
var obj = new Object();
console.log(obj instanceof Map); // false
console.log(map instanceof Object); // true
var map = new Map();//Empty
map.set(1,'1');
map.set('one', 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)
let obj ={
1:'1',
'one':1,
'{}': {name:'Hello world'},
12.3:12.3,
[12]:[100]
}
console.log(obj)