多使用Map,减少Object(译文)

95 阅读4分钟

image.png Javascript中,可以在任何地方使用Object,但是仅仅可以使用并不意味着就一定要使用。

// 🚩
const mapOfThings = {}

mapOfThings[myThing.id] = myThing

delete mapOfThings[myThing.id]

例如,如果您在JavaScript中使用对象来存储任意键值对,并经常在对象中添加或删除key,那么您应该真正考虑使用Map而不是plain Object。

// ✅
const mapOfThings = new Map()

mapOfThings.set(myThing.id, myThing)

mapOfThings.delete(myThing.id)

使用objects的性能问题

对于object,delete运算符因性能差而臭名昭著, 上面的例子中,使用maps可以提升性能, 并且在其他一些情况下可以获得非常大的性能提升。

image.png

上面的对比图是一个 基准案例 (运行在 Chrome v109版本, Core i7 MBP). 你也可以对比这个案例 另一个基准 来自 Zhenghao He. 考虑一下 — 类似上面的基准是  众所周知的不完美的 所以还应该保持一个怀疑态度。

你不需要相信我或者任何人的基准, 在 MDN Maps的优化案例中 有如下描述:

image.png (Map在在频繁添加和删除键值对的情况下,性能更好。Object未针对频繁添加和删除键值对进行优化。)

如果你好奇为什么,这与JavaScriptVM如何通过假设JS Object的形状来优化JS对象有关,而Map是专门为哈希映射的用例构建的,其中键是动态和不断变化的。

除了性能,map还可以解决Object存在的几个问题。

Built-in keys problem(内置属性问题)

一个主要的问题就是:在object刚创建好时,就已经被很多内置属性和方法污染了。

const myMap = {}

myMap.valueOf // => [Function: valueOf]
myMap.toString // => [Function: toString]
myMap.hasOwnProperty // => [Function: hasOwnProperty]
myMap.isPrototypeOf // => [Function: isPrototypeOf]
myMap.propertyIsEnumerable // => [Function: propertyIsEnumerable]
myMap.toLocaleString // => [Function: toLocaleString]
myMap.constructor // => [Function: Object]

所以即使你创建一个空的对象,这些方法也已经被创建了。所以当使用Object实现一个任意key值的hashMap会导致很多问题,后面会有相关介绍。

笨拙的迭代

JavaScript对象处理键的方式非常奇怪,遍历Object充满了陷阱。

例如,您可能已经知道不要这样做:

for (const key in myObject) {
  // 🚩 你可能会发现一些你无意中继承的key值
}

你可能被告知要这样做:

for (const key in myObject) {
  if (myObject.hasOwnProperty(key)) {
    // 🚩
  }
}

但是这也是有问题的, 例如 myObject.hasOwnProperty 函数也是可以被重载的,比如 myObject.hasOwnProperty = () => explode().

因此,这才是你应该做的:

for (const key in myObject) {
  if (Object.prototype.hasOwnProperty.call(myObject, key) {
    // 😕
  }
}

或者,如果您希望代码更加简洁,可以使用最近添加的Object.hasOwn

for (const key in myObject) {
  if (Object.hasOwn(myObject, key) {
    // 😐
  }
}

或者你可以放弃使用 for 循环,转而使用 Object.keys 和 forEach.

Object.keys(myObject).forEach(key => {
  // 😬
})

但是,如果使用Maps,以上问题都不存在. 你可以使用标准的 for 循环,使用标准迭代器和一个非常好的析构函数模式,可以同时获得keyvalue

for (const [key, value] of myMap) {
 // 😍
}

事实上,这非常好,我们现在有一个Object.entries方法来处理对象。

for (const [key, value] of Object.entries(myObject)) {
 // 🙂
}

使用Map,也可以做到

for (const value of myMap.values()) {
 // 🙂
}

for (const key of myMap.keys()) {
 // 🙂
}

key值顺序

Map中的键以简单、直接的方式排序:Map对象按条目插入顺序排列entry、key和value。 这给了我们另一个非常酷的特性,那就是我们可以直接从地图中按键的精确顺序销毁键:

// 这个方法就可以获取到myMap的第一个插入的key和value值
const [[firstKey, firstValue]] = myMap
// 获取多个
const [[firstKey, firstValue],[secondKey, secondValue], [thirdKey, thirdValue]] = myMap

复制

你可能会说,Object的复制非常简单

const copied = {...myObject}
const copied = Object.assign({}, myObject)

但是Maps的复制也很简单

const copied = new Map(myMap)

这个能生效的原因是 Map 通过 [key, value] 元组(tuples)构造. 相反,Map也可以通过[key, value]的元组结构进行遍历.

而且,也可以像Object一样,通过 structuredClone进行深拷贝:

const deepCopy = structuredClone(myMap)

Maps和Object的相互转化

Maps转Object可以通过 Object.fromEntries实现(这个方法还挺好用的):

const myObj = Object.fromEntries(myMap)

Object转Maps, 使用 Object.entries:

const myMap = new Map(Object.entries(myObj))

Key 类型

可以使用各种数据结构当作key值

myMap.set({}, value)
myMap.set([], value)
myMap.set(document.body, value)
myMap.set(function() {}, value)
myMap.set(myDog, value)

WeakMaps

我们可以使用 WeakMap 类型. weakMap通过使用弱引用(weak reference),解决内存泄露的问题.

当其他引用被释放后, weakMap中的对象将会被垃圾回收机制回收

const metadata = new WeakMap()

// ✅ 没有内存泄露, myTodo 将自动释放当不存在其他的引用时
metadata.set(myTodo, {
  focused: true
})

原文出处:www.builder.io/blog/maps#b…