以下文章来源于大迁世界 ,作者前端小智
掘金LV8,思否10万+的作者。一个热爱前端的创业者。
作者 | 大迁世界
在 JavaScript 中,对象是很方便的。它们允许我们轻松地将多个数据块组合在一起。在ES6之后,又出了一个新的语言补充-- Map。在很多方面,它看起来像是一个功能更强的对象,但接口却有些笨拙。
然而,大多数开发者在需要 hash map 的时候还是会使用对象,只有当他们意识到键值不能只是字符串的时候才会转而使用 Map。因此,Map 在当今的 JavaScript 社区中仍然没有得到充分的使用。
在本文本中,我会列举一些应该更多考虑使用 Map 的一些原因。
在 Hash Map 中使用对象最明显的缺点是,对象只允许键是字符串和 symbol。任何其他类型的键都会通过 toString
方法被隐含地转换为字符串。
const foo = []
const bar = {}
const obj = {[foo]: 'foo', [bar]: 'bar'}
console.log(obj) // {"": 'foo', [object Object]: 'bar'}
更重要的是,使用对象做 Hash Map 会造成混乱和安全隐患。
在ES6之前,获得 hash map 的唯一方法是创建一个空对象:
const hashMap = {}
然而,在创建时,这个对象不再是空的。尽管 hashMap
是用一个空的对象字面量创建的,但它自动继承了 Object.prototype
。这就是为什么我们可以在 hashMap
上调用hasOwnProperty
、toString
、constructor
等方法,尽管我们从未在该对象上明确定义这些方法。
由于原型继承,我们现在有两种类型的属性被混淆了:存在于对象本身的属性,即它自己的属性,以及存在于原型链的属性,即继承的属性。
因此,我们需要一个额外的检查(例如hasOwnProperty
)来确保一个给定的属性确实是用户提供的,而不是从原型继承的。
除此之外,由于属性解析机制在 JavaScrip t中的工作方式,在运行时对 Object.prototype
的任何改变都会在所有对象中引起连锁反应。这就为原型污染攻击打开了大门,这对大型的JavaScript 应用程序来说是一个严重的安全问题。
不过,我们可以通过使用 Object.create(null)
来解决这个问题,它可以生成一个不继承Object.prototype
的对象。
当一个对象自己的属性与它的原型上的属性有名称冲突时,它就会打破预期,从而使程序崩溃。
例如,我们有一个函数 foo
,它接受一个对象。
function foo(obj) {
//...
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
}
}
}
obj.hasOwnProperty(key)
有一个可靠性风险:考虑到属性解析机制在JavaScript中的工作方式,如果 obj
包含一个开发者提供的具有相同名称的 hasOwnProperty
属性,那就会对Object.prototype.hasOwnProperty
产生影响。因此,我们不知道哪个方法会在运行时被准确调用。
可以做一些防御性编程来防止这种情况。例如,我们可以从 Object.prototype
中 "借用""真正的 hasOwnProperty
来代替:
function foo(obj) {
//...
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// ...
}
}
}
还有一个更简短的方法就是在一个对象的字面量上调用该方法,如{}.hasOwnProperty.call(key)
,不过这也挺麻烦的。这就是为什么还会新出一个静态方法Object.hasOwn
的原因了。
Object
没有提供足够的人机工程学,不能作为 hash map 使用,许多常见的任务不能直观地执行。
Object
并没有提供方便的API来获取 size
,即属性的数量。而且,对于什么是一个对象的 size ,还有一些细微的差别:
如果只关心字符串、可枚举的键,那么可以用 Object.keys()
将键转换为数组,并获得其length
如果k只想要不可枚举的字符串键,那么必须得使用 Object.getOwnPropertyNames
来获得一个键的列表并获得其 length
如果只对 symbol 键感兴趣,可以使用 getOwnPropertySymbols
来显示 symbol 键。或者可以使用 Reflect.ownKeys
来一次获得字符串键和 symbol 键,不管它是否是可枚举的。
for...in
循环。但它会读取到继承的可枚举属性。Object.prototype.foo = 'bar'
const obj = {id: 1}
for (const key in obj) {
console.log(key) // 'id', 'foo'
}
for ... of
,因为默认情况下它不是一个可迭代的对象,除非我们明确定义 Symbol.iterator
方法在它上面。Object.keys
、Object.values
和 Object.entry
来获得一个可枚举的字符串键(或/和值)的列表,并通过该列表进行迭代,这引入了一个额外的开销步骤。const obj = {}
obj.foo = 'first'
obj[2] = 'second'
obj[1] = 'last'
console.log(obj) // {1: 'last', 2: 'second', foo: 'first'}
delete
操作符一个一个地删除每个属性,这在历史上是众所周知的慢。undefined
。相反,得使用 Object.prototype.hasOwnProperty
或 Object.hasOwn
。const obj = {a: undefined}
Object.hasOwn(obj, 'a') // true
Map.prototype.get
来获取对应的项。for ... of
轻松地迭代一个 Map,并做一些事情,比如使用嵌套的解构来从 Map 中取出第一个项。const [[firstKey, firstValue]] = map
Map.prototype.has
检查一个给定的项是否存在,与必须在对象上使用Object.prototype.hasOwnProperty/Object.hasOwn
相比,不那么尴尬了。
Map.prototype.get 返回与提供的键相关的值。有的可能会觉得这比对象上的点符号或括号符号更笨重。不过,它提供了一个干净的用户数据和内置方法之间的分离。
Map.prototype.size
返回 Map 中的项的个数,与获取对象大小的操作相比,这明显好太多了。此外,它的速度也更快。
Map.prototype.clear
可以删除 Map 中的所有项,它比 delete 操作符快得多。
Map
要比 Object
快。有些人声称通过从 Object 切换到 Map 可以看到明显的性能提升。measureFor
,它重复运行目标函数,直到达到指定的最小时间阈值(即用户界面上的 duration
输入字段)。它返回这样一个函数每秒钟被执行的平均次数。function measureFor(f, duration) {
let iterations = 0;
const now = performance.now();
let elapsed = 0;
while (elapsed < duration) {
f();
elapsed = performance.now() - now;
iterations++;
}
return ((iterations / elapsed) * 1000).toFixed(4);
}
delete
操作符从一个对象中删除所有属性所需的时间,并与相同大小的 Map 使用 Map.prototype.delete
的时间进行比较。也可以使用Map.prototype.clear
,但这有悖于基准测试的目的,因为我知道它肯定会快得多。for ... in
循环。Math.random().toString()
生成的数字字符串,例如:0.4024025689756525。toString
明确地将其转换为字符串,以避免隐式转换的开销。Object
和 Map
开始,一直到 5000000,并让每种类型的操作持续运行 10000ms,看看它们之间的表现如何。下面是测试结果:Map
在所有操作上都优于 Object
。100000
),Map 在插入速度上 是Object 的两倍,但当规模超过 100000
时,性能差距开始缩小。5000000时
,Map 只快了 30%。[0, 1000]
范围内的整数键。65%
,迭代速度快16%
。 Object/Map
size 和整数键范围的不同组合,但没有想出一个明确的模式。但我看到的总体趋势是,随着 size 的增长,以一些相对较小的整数作为键值,Object
在插入方面比Map
更有性能,在删除方面总是大致相同,迭代速度慢4或5倍。Math.random().toString()
生成的数字字符串。node --expose-gc
运行它,就得到了以下结果。{
object: {
'string-key': {
'10000': 3.390625,
'50000': 19.765625,
'100000': 16.265625,
'500000': 71.265625,
'1000000': 142.015625
},
'numeric-key': {
'10000': 1.65625,
'50000': 8.265625,
'100000': 16.765625,
'500000': 72.265625,
'1000000': 143.515625
},
'integer-key': {
'10000': 0.25,
'50000': 2.828125,
'100000': 4.90625,
'500000': 25.734375,
'1000000': 59.203125
}
},
map: {
'string-key': {
'10000': 1.703125,
'50000': 6.765625,
'100000': 14.015625,
'500000': 61.765625,
'1000000': 122.015625
},
'numeric-key': {
'10000': 0.703125,
'50000': 3.765625,
'100000': 7.265625,
'500000': 33.265625,
'1000000': 67.015625
},
'integer-key': {
'10000': 0.484375,
'50000': 1.890625,
'100000': 3.765625,
'500000': 22.515625,
'1000000': 43.515625
}
}
}
writable
/enumerable
/configurable
。Map 比 Object 快,除非有小的整数、数组索引的键,而且它更节省内存。
如果你需要一个频繁更新的 hash map,请使用 Map;如果你想一个固定的键值集合(即记录),请使用Object,并注意原型继承带来的陷阱。
点分享 点收藏 点点赞 点在看