原来,Map 的真相居然是 ...

2022-03-19  本文已影响0人  这波能反杀

大家好,我是这波能反杀,一名光荣的十年老前端。

这两天,我已经麻了。

当我准备集中精力完成《React 知命境》的时候,一个不起眼的小问题,幽幽的复现在我的眼前。

我的第一反应,这么简单的问题,想要用的时候就用呗。

但是... ... 不对,在我模糊的印象中,这个人能力挺强,常年看我的文章,对基础的研究比较深入,不会问那么随便的问题,搞不好我这样敷衍了之后,还会被引出下一个问题,我得回答专业一点,稳住我十年老油子的高大人设才行。

根据我的预判,她一定是想问什么时候用 Object,什么时候用 Map,在 JavaScript 的使用者中,有此疑问的不在少数。于是我就开始在记忆中搜索关于这两个对象的区别。

ObjectMap 都可以存储键值对。

Objectkey 值只能是数字、字符串,symbol。Map 的key值可以是任意数据类型。

Map 是可迭代对象,Object 不可以迭代。

Map 会记录属性的写入顺序,Object 不会记录写入的先后顺序甚至还有可能会排序。

Map 有 size 属性,而 Object 没有。

那什么时候用 MapObject 更合适呢?

额... ... 糟糕,想不起来!

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 来管理数据再合适不过了。

太棒了。有了这些东西,我终于松了一口气。可以回复她了。

可是,当我点开聊天窗口的时候... ...

我是这波能反杀,关注我,解锁更多... ... 哎,算了...

上一篇下一篇

猜你喜欢

热点阅读