1// Map2
3// 初始化4const map = new Map()5// 添加属性6map.set('name', 'map')7// 删除属性8map.delete('name')
这两个达到的效果是差不多的,但他们在语法上却不一样,Map
的语法看上去更符合函数式开发的风格,而 Object
的语法看上去更像是命令式的代码。我们再来看看他们在相同操作下执行 100000
次的情况下的性能差异:
1// Object 的执行代码2const object = {}3for(let i = 0; i < 100000; i++){4 object.a = i;5 delete object.a6}7
8// Map 的执行代码9const map = new Map()10for(let i = 0; i < 100000; i++){3 collapsed lines
11 map.set('a', i)12 map.delete('a')13}
除了这种测试方式之外,我还找到了另一种测试方式(example benchmark
)。当然了,这种基准测试的结果是不可靠的,因为它们依赖于 JavaScript 引擎的实现,也同时和浏览器的版本有关系,客户端的机器配置也会影响到测试结果,所以我们只能用它们来做一个参考。也就是说,你完全可以不相信任何人的测试结果,因为在 MDN
上的 Map 一节中也有提到过 Map
在频繁增删键值对的场景下是有特殊优化的,而 Object
则没有。
Map | Object | |
---|---|---|
省略… | 省略… | 省略… |
Performance | Performs better in scenarios involving frequent additions and removals of key-value pairs.(在频繁增删键值对的场景下表现更好。) | Not optimized for frequent additions and removals of key-value pairs.(在频繁添加和删除键值对的场景下未作出优化。) |
当然如果是只有这一点微乎其微的性能,那可能很难说服你使用 Map
,但接下来的内容可能可以改变你的想法。
1const obj = { toString: 'hello' }2
3const map = new Map([['toString', 'hello']])4
5console.log(obj.toString) // ?6console.log(map.toString) // ?
这里的 obj
和 map
都有一个 toString
属性,但是它们的值是不一样的,obj
的 toString
属性是一个字符串,而 map
的 toString
属性是一个函数,对于 obj
来讲,自行设置的 toString
会覆盖原型链上的属性,而 map
中设置的 toString
键却不会。我认为仅此一点就足以说明 Map
的优势了,因为 Map
中的键值对是不会被覆盖的,而 Object
中的键值对则会被覆盖。
在 JS 中迭代对象的时候,我们通常会使用 for...in
循环,但是这种方式有一个陷阱,就是它会遍历原型链上的属性,而 Map
中的 for...of
循环则不会遍历原型链上的属性,所以 Map
更加安全。
1// 正常情况下你可能会这么做2for (const key in obj) {3 console.log(key)4}5
6// 经验丰富一点的你可能会这么做7for (const key in obj) {8 if (obj.hasOwnProperty(key)) {9 console.log(key)10 }1 collapsed line
11}
但这么做依然是有问题的,因为 hasOwnProperty
方法也是从原型链上继承来的,没人能保证 hasOwnProperty
方法不会被覆盖。所以你最终可能需要这么做:
1for (const key in obj) {2 if (Object.prototype.hasOwnProperty.call(obj, key)) {3 console.log(key)4 }5}
当然如果你不想这么麻烦,则可以完全放弃 for...in
循环,进而采用 Object.keys
和 forEach
:
1Object.keys(obj).forEach(key => {2 console.log(key)3})
但 Map
就不存在这些问题,因为它的 for...of
循环不会遍历原型链上的属性,所以你可以放心的使用 for...of
循环来遍历 Map
。
1for (const [key, value] of map) {2 console.log(key, value)3}
在 JS 中,对象的键值对是无序的,但 Map
中的键值对是有序的,这意味着你可以通过 Map
来实现一个有序的对象。
1const [2 [key1, value1],3 [key2, value2],4 [key3, value3]5] = new Map([6 ['a', 1],7 ['b', 2],8 ['c', 3]9])10
3 collapsed lines
11console.log(key1, value1) // a 112console.log(key2, value2) // b 213console.log(key3, value3) // c 3
在 JS 中,对象的拷贝是浅拷贝,而 Map
的拷贝是深拷贝,这意味着你可以通过 Map
来实现一个深拷贝的对象。
1const copied = {...obj}2const copied = Object.assign({}, obj)
简简单单对吧,但其实 Map 也很容易实现拷贝功能:
1const copied = new Map(map)
而之所以可以这么做,主要是因为 Map
的构造函数接收了一个可迭代的 [[key, value]]
元组,所以 Map
也可以接收一个 Map
实例,这样就可以实现深拷贝了。当然,Map
也是支持使用浏览器内置原生支持的 structuredClone
方法来实现深拷贝的,但因为兼容性的问题,所以这里就不展开了。语法如下:
1const copied = structuredClone(map)2const copied = new Map(map, [structuredClone])
1const obj = { a: 1, b: 2, c: 3 }2
3// Object to Map4const map = new Map(Object.entries(obj))5
6// Map to Object7const obj = Object.fromEntries(map)
在你理解了这种转换欢喜以后,你就可以用对象的形式来构造一个 Map
了:
1// 原本你可能会这么做2const myMap = new Map([['key', 'value'], ['keyTwo', 'valueTwo']])3
4// 现在你可以这么做5const myMap = new Map(Object.entries({6 key: 'value',7 keyTwo: 'valueTwo'8}))9
10// 你还可以封装一个函数6 collapsed lines
11const makeMap = obj => new Map(Object.entries(obj))12
13const myMap = makeMap({14 key: 'value',15 keyTwo: 'valueTwo'16})
如果你需要使用 TypeScript
,那你可以这么做:
1const makeMap = <Value = unknown>(obj: Record<string, Value>) => new Map<string, Value>(Object.entries(obj))2
3const myMap = makeMap({ key: 'value' }) // => Map<string, string>
在 JS 中,对象的键名只能是字符串或者 Symbol,而 Map
的键名可以是任意值,这意味着你可以使用 Map
来实现一个类似于 WeakMap
的功能,但是 Map
可以使用任意值作为键名,而 WeakMap
只能使用对象作为键名。
1const map = new Map()2const key = {}3map.set(key, 'value')4console.log(map.get(key)) // value
这适用于想将数据扁平化从而建立一种数据联结的场景,你可以以整个对象作为键名,同时将对象的某个属性作为键值,这样你就既可以使用对象来获取键值,也可以使用键值来获取对象了。
当然,Map 上还有很多有用的属性:
size
:返回 Map
中的键值对的数量。clear()
:移除 Map
对象中的所有键值对。keys()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的键。values()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的值。entries()
:返回一个新的 Iterator
对象,它按插入 Map
对象中的顺序包含 Map
对象中每个元素的 [key, value]
数组。forEach()
:对 Map
对象中的每个键值对执行指定的操作。当我们讨论 Map
时,我们也可以讨论一下 Set
,因为它们是一对好基友,它们的功能也是相似的,但是 Set
只有键名,没有键值,所以 Set
中的键名和键值是相同的。
1const set = new Set([1, 2, 3])2
3set.add(3)4set.delete(4)5set.has(5)
在某些情况下,Set
可以完全替代 Array
做一些等效操作,且拥有 更好的性能。当然这种结果可能并不准确,所以你可以自己测试一下。同样的,我们可以使用 WeakSet
来帮我们解决内存泄漏的问题。
1const checkedTodos = new Set([todo1, todo2, todo3])
Map
和 Set
都是可序列化的,这意味着你可以使用 JSON.stringify
来序列化它们,但是你需要注意的是,Map
和 Set
中的键名和键值都必须是可序列化的,否则就会抛出错误。但是你有没有注意到,如果你想要打印出带有缩进格式或者其他任意风格的 JSON 时,你总是需要添加一个 null
作为参数,而这个 null
被称之为替换器。如果你想要使用替换器,你可以使用 JSON.stringify
的第二个参数来指定替换器,或者使用第三个参数来指定缩进空格的数量。这两个参数都是可选的,但是如果你想要使用第三个参数,你就必须使用第二个参数来指定替换器,即使你不需要使用替换器。
1JSON.stringify(obj, null, 2)
我们可以写一个转换器来解析 Map 和 Set,这样我们就可以使用 JSON.stringify
来序列化它们了,同时增加一个特殊属性来标识它们的类型。
1function replacer(key, value) {2 if (value instanceof Map) {3 return { __type: 'Map', value: Object.fromEntries(value) }4 }5 if (value instanceof Set) {6 return { __type: 'Set', value: Array.from(value) }7 }8 return value9}10
14 collapsed lines
11function reviver(key, value) {12 if (value?.__type === 'Set') {13 return new Set(value.value)14 }15 if (value?.__type === 'Map') {16 return new Map(Object.entries(value.value))17 }18 return value19}20
21const obj = { set: new Set([1, 2]), map: new Map([['key', 'value']]) }22const str = JSON.stringify(obj, replacer)23const newObj = JSON.parse(str, reviver)24// { set: new Set([1, 2]), map: new Map([['key', 'value']]) }
最后,我们再来讨论一下,在什么时候、什么场景该用哪一个。
Object
。Map
。Set
。WeakSet
。WeakMap
。Array
。Array
,因为 Set
是无序的。以上。