原来,Map 的真相居然是 ...
大家好,我是这波能反杀,一名光荣的十年老前端。
这两天,我已经麻了。
当我准备集中精力完成《React 知命境》的时候,一个不起眼的小问题,幽幽的复现在我的眼前。
我的第一反应,这么简单的问题,想要用的时候就用呗。
但是... ... 不对,在我模糊的印象中,这个人能力挺强,常年看我的文章,对基础的研究比较深入,不会问那么随便的问题,搞不好我这样敷衍了之后,还会被引出下一个问题,我得回答专业一点,稳住我十年老油子的高大人设才行。
根据我的预判,她一定是想问什么时候用 Object
,什么时候用 Map
,在 JavaScript
的使用者中,有此疑问的不在少数。于是我就开始在记忆中搜索关于这两个对象的区别。
Object
和 Map
都可以存储键值对。
Object
的 key
值只能是数字、字符串,symbol。Map
的key值可以是任意数据类型。
Map
是可迭代对象,Object
不可以迭代。
Map
会记录属性的写入顺序,Object
不会记录写入的先后顺序甚至还有可能会排序。
Map
有 size 属性,而 Object 没有。
那什么时候用 Map
比 Object
更合适呢?
额... ... 糟糕,想不起来!
CPU 单线程运转了五分钟,还是想不起来。不过没关系,我还有万能的百度,我输入关键字,开始搜索,我很快就能得到答案。
果然百度大法好,搜索结果里有大量的文章在分析他们,头一条是掘金的一篇译文,一看就比较靠谱,于是满怀期待的点进去,开始阅读这篇文章。
文章在分析了大量的各自的特点之后,终于看到了我想要的内容,Map 的应用场景。
然而读着读着,好像有点不太对劲。更多的还是一些特性的分析,例如 Map
的删除操作性能更好,存储大数更合适,并没有介绍任何具体的场景。
而且还出现了一些我不太认同的小疑问。
好吧,这篇文章没有我找到的答案,如法炮制,我开始阅读别的文章。
然而... 大多数都是雷同的信息。
我依稀记得之前使用 Map 解决过一个非常棒的案例,可就是想不起来,加上搜索无望,我不免有些心急。
时间已经不知不觉过去了 10 分钟,我还没有回复那个尊称我为“波神”的小妹妹。我已经感受到了,我的大佬人设正在逐渐崩塌。
对哦,我可太蠢了,既然我之前使用过,把项目代码拿出来搜索一下不就找到了吗?
看到了希望的曙光,我进入了修改紧急 bug 的高效状态。迅速打开 vs code
,打开我的项目代码,全局搜索 new Map
。
哈哈,果然找到了大量使用 Map
的代码。
可是当我点开代码之后,才发现.... 居然没有熟悉感。全都不是我想要的场景。
这样的情况,用 {}
不更简单吗?
这肯定不是我写的代码。
真正的大师,永远怀着一颗学徒的心。
我实在想不起来以前那个非常适合用 Map 的场景了,搜索找不到,代码也找不到,好在我还有几个大厂大佬众多,且非常活跃的技术群。
于是我问.
我再问.
这个问题在几个群都引发了激烈的讨论,我就像一个小白疯狂的吸收着大佬们的知识。大佬之间的讨论就是不一样,很快我们就撇开了毫无意义的表面特性,开始聊起了性能。
有个字节的大佬抛出一个观点,他说,对象的读取没有 Map 快,所以他几乎都是能用 Map 的地方就会用 Map。
另一个大佬认为在速度上 Map 比 Object 并没有明显优势,在删除属性时 Map 表现更好一点。
我又想到了刚才看的掘金的文章,说是 Object 的读写速度更快,几个结论说法不一,于是讨论陷入了验证阶段。
如果这个事情能够得到论证的话,那么「能用 Map 的地方就使用 Map」 就是一个非常完美的答案。
为了证明这个事情,我开始考虑一个事情,Object 在内存中到底是如何存储的?Map 又是如何存储的呢?
我依稀记得 V8 对 Object 的处理是有优化手段的,但是年代久远记不清晰了,于是有了新的方向,我再次踏上了寻找资料的征途。再次祭出百度。
果然不出我所料。
V8 对对象属性的存储结构并没有表面上那么简单,有特殊的处理。
在掘金和知乎的文章里,我找到这个图。
一个对象里有快属性和快属性的区分。当属性数量较少时「in-object properties」,或者 key 为数字类型「elements」,此时会采用快属性,快属性使用线性结构存储。所以读取属性的速度是非常快的。当属性变多,为了确保新增和删除的效率,此时会启用慢属性「properties」,采用词典键值对的方式存储属性内容。
我知道,很多人看到这里,肯定会疑问什么是线性结构,什么是非线性结构,哈哈哈,还好我没有疑问。
也就是说,从基础理论上来看,Object 会因为属性数量上分为两个阶段,从而解决 Object 的读写问题,而且,V8 还为 Object 创建了隐藏类用于记录每个属性的偏移量,也就意味着,Object 的读写不会太慢。
这个界限有的文章说是10个,但是我使用开发者工具的 Memery 记录的 snapshot 验证的结果不是这样
那么 Map 的存储在内存中又是什么结构呢?
哈哈,这个我知道!
散列表 + 链表 + 红黑树 = hashMap。
我突然就悟了。
也就是说,如果属性数量偏小的情况下,读写速度上,Object 和 Map 应该不会有太大的差别。而只有数据量非常大的时候,才会逐渐体现出来差别。那么这个数据量大,到底要达到什么程度呢?我也不确定。
写个案例,验证一下。
首先验证一下写入的时间成本。
// 先定义一个数量上限
const up = 9999
var mt1 = performance.now()
var map = new Map()
for(var i = 0; i < up; i++) {
map.set(`f${i}`, {a: i, children: { a: i }})
}
console.log(` Map: `, performance.now() - mt1)
var ot1 = performance.now()
var obj = {}
for (var i = 0; i < up; i++) {
obj[`f${i}`] = {b: i, children: {a: i}}
}
console.log('Object: ', performance.now() - ot1)
刷新了 20 多次,基本上都在 5 ~ 10 ms 之间波动。时间上谁高谁低没有明确的差别。不过 Object 比 Map 耗时更短的次数会多一点。这符合我的预期。
接下来我组建调高 up 变量的值。继续验证。当我把 up 的值设置为 99999 时,耗时上依然没有明显的差别。
当我把 up 的值设置为 499999
时,Object 的写入速度才开始稳定的比 Map 耗时更长。
好家伙,我从来不会维护这么大的数据量在项目中。
接下来,我又依次验证的读取速度和删除速度。
在删除上,我把 up 的值设置为 199999
,Object 的删除耗时才会稳定的比 Map 慢。
// 删除
for(var i = 0; i < up; i++) {
map.delete(`f${i}`)
}
在读取速度上,up 的值为 159999
时 Object 的读取速度会稳定比 Map 慢。
// 访问
for (var i = 0; i < up; i++) {
map.get(`f${i}`)
}
所以,在性能的表现上,新增、删除、读取的速度,在数量非常少时,Object 的表现可能会稍微好那么一点点点,甚至不太明显能感知得出来。
而在数量非常大的时候,Map 的表现会比 Object 好。可是这种程度的数量,我想很难在项目中把数据维护到这种程度。
验证结果让我居然神奇的发现,上面两位大佬不一样的观点,居然都说的过去。
能用 Map 就用 Map ,没什么毛病。Object 也没有比 Map 有什么明显的速度优势。
当我做完验证回过头来看群消息的时候,另外一个群的大佬提供了一个非常牛逼的应用场景。那就是策略模式的封装。
膜拜!
策略模式通常情况下都是一个键值对应一个规则。但是!在某些特殊场景下,会出现多个键值对应一个规则的情况。这个时候,Map 就有了用武之地。Map 支持正则表达式作为 key 值,这样,使用 Map 就可以存储多对一的匹配规则。
折腾了一天,我苦逼的发现,我终于想起来我之前用 Map 实现的那个应用场景是什么了。
( |||)
那就是聊天列表和聊天内容的实现。这个场景完美符合了 Map 的特性。
聊天内容列表因为要缓存很多内容,数据量够大,并且,聊天是一个频繁变动的场景。聊天列表有新消息就会重新排序,聊天内容也会频繁的插入新的消息,特别是群聊,对聊天内容的顺序也有严格的要求。
数据量大、频繁写入、排序、对写入顺序有严格的要求,这个场景使用 Map 来管理数据再合适不过了。
太棒了。有了这些东西,我终于松了一口气。可以回复她了。
可是,当我点开聊天窗口的时候... ...
我是这波能反杀,关注我,解锁更多... ... 哎,算了...