Object 和 Map| ,在JS 项目中给你选,你会选??

在日常的 JavaScript 项目中,我们最常用到的数据结构就是各种形式的键值对格式了(key-value pair)。在 JavaScript 中,除了最基础的Object是该格式外,ES6 新增的Map也同样是键值对格式。它们的用法在很多时候都十分接近。不知道有没有人和我一样纠结过该选择哪个去使用呢?在本菜最近的项目中,我又遇到了这样的烦恼,索性一不做二不休,去对比一下究竟该使用哪一个。

本文将会探讨一下Object和Map的不同,从多个角度对比一下Object和Map:

用法的区别:在某些情况下的用法会截然不同

句法的区别:创建以及增删查改的句法区别

性能的区别:速度和内存占用情况

希望读完本文的你可以在日后的项目中做出更为合适的选择。

用法对比

对于Object而言,它键(key)的类型只能是字符串,数字或者Symbol;而对于Map而言,它可以是任何类型。(包括 Date,Map,或者自定义对象)

Map中的元素会保持其插入时的顺序;而Object则不会完全保持插入时的顺序,而是根据如下规则进行排序:

非负整数会最先被列出,排序是从小到大的数字顺序

然后所有字符串,负整数,浮点数会被列出,顺序是根据插入的顺序

最后才会列出Symbol,Symbol也是根据插入的顺序进行排序的

读取Map的长度很简单,只需要调用其.size()方法即可;而读取Object的长度则需要额外的计算:Object.keys(obj).length

Map 是可迭代对象,所以其中的键值对是可以通过for of循环或.foreach()方法来迭代的;而普通的对象键值对则默认是不可迭代的,只能通过for in循环来访问(或者使用Object.keys(o)、Object.values(o)、Object.entries(o)来取得表示键或值的数字)迭代时的顺序就是上面提到的顺序。

consto = {};constm =newMap();o[Symbol.iterator] !==undefined;// falsem[Symbol.iterator] !==undefined;// true复制代码

在Map中新增键时,不会覆盖其原型上的键;而在Object中新增键时,则有可能覆盖其原型上的键:

Object.prototype.x =1;consto = {x:2};constm =newMap([[x,2]]);o.x;// 2,x = 1 被覆盖了m.x;// 1,x = 1 不会被覆盖复制代码

JSON默认支持Object而不支持Map。若想要通过JSON传输Map则需要使用到.toJSON()方法,然后在JSON.parse()中传入复原函数来将其复原。

对于JSON这里就不具体展开了,有兴趣的朋友可以看一下这:JSON 的序列化和解析

consto = {x:1};constm =newMap([['x',1]]);consto2 =JSON.parse(JSON.stringify(o));// {x:1}constm2 =JSON.parse(JSON.stringify(m))// {}复制代码

句法对比

创建时的区别

Obejct

consto = {};// 对象字面量consto =newObject();// 调用构造函数consto =Object.create(null);// 调用静态方法 Object.create 复制代码

对于Object来说,我们在 95%+ 的情况下都会选择对象字面量,它不仅写起来最简单,而且相较于下面的函数调用,在速度方面会更为高效。对于构建函数,可能唯一使用到的情况就是显式的封装一个基本类型;而Object.create可以为对象设定原型。

Map

constm =newMap();// 调用构造函数复制代码

和Object不同,Map没有那么多花里胡哨的创建方法,通常只会使用其构造函数来创建。

除了上述方法之外,我们也可以通过Function.prototype.apply()、Function.prototype.call()、reflect.apply()、Reflect.construct()方法来调用Object和Map的构造函数或者Object.create()方法,这里就不展开了。

新增/读取/删除元素时的区别

Obejct

consto = {};//新增/修改o.x =1;o['y'] =2;//读取o.x;// 1o['y'];// 2//或者使用 ES2020 新增的条件属性访问表达式来读取o?.x;// 1o?.['y'];// 2//删除deleteo.b;复制代码

对于新增元素,看似使用第一种方法更为简单,不过它也有些许限制:

属性名不能包含空格和标点符号

属性名不能以数字开头

对于条件属性访问表达式的更多内容可以看一下这:条件属性访问表达式

Map

constm =newMap();//新增/修改m.set('x',1);//读取map.get('x');//删除map.delete('b');复制代码

对于简单的增删查改来说,Map上的方法使用起来也是十分便捷的;不过在进行联动操作时,Map中的用法则会略显臃肿:

constm =newMap([['x',1]]);// 若想要将 x 的值在原有基础上加一,我们需要这么做:m.set('x', m.get('x') +1);m.get('x');// 2consto = {x:1};// 在对象上修改则会简单许多:o.x++;o.x// 2复制代码

性能对比

接下来我们来讨论一下Object和Map的性能。不知道各位有没有听说过 Map 的性能优于 Object 的说法,我反正是见过不少次,甚至在 JS 高程四中也提到了Map对比Object时性能的优势;不过对于性能的概括都十分的笼统,所以我打算做一些测试来对比一下它们的区别。

测试方法

在这里我进行的对于性能测试的都是基于v8 引擎的。速度会通过 JS 标准库自带的performance.now()函数来判断,内存使用情况会通过Chrome devtool中的memory来查看。

对于速度测试,因为单一的操作速度太快了,很多时候performance.now()会返回 0。所以我进行了 10000 次的循环然后判断时间差。因为循环本身也会占据一部分时间,所以以下的测试只能作为一个大致的参考。

创建时的性能

测试用的代码如下

letn,  n2 =5;// 速度while(n2--) {letp1 = performance.now();  n =10000;while(n--) {leto = {}; }letp2 = performance.now();  n =10000;while(n--) {letm =newMap(); }letp3 = performance.now();console.log(`Object:${(p2 - p1).toFixed(3)}ms, Map:${(p3 - p2).toFixed(3)}ms`);}// 内存classTest{}lettest =newTest();test.o = o;test.m = m;复制代码

首先进行对比的是创建Object和Map时的表现。对于创建的速度表现如下:

我们可以发现创建Object的速度会快于Map。对于内存使用情况则如下:

我们主要关注其Retained Size,它表示了为其分配的空间。(即删除时释放的内存大小)

通过对比我们可以发现,空的Object会比空的Map占用更少的内。所以这一轮Object赢得一筹。

新增元素时的性能

测试用的代码如下

console.clear();letn,  n2 =5;leto = {}, m =newMap();// 速度while(n2--) {letp1 = performance.now();  n =10000;while(n--) { o[Math.random()] =Math.random(); }letp2 = performance.now();  n =10000;while(n--) { m.set(Math.random(),Math.random()); }letp3 = performance.now();console.log(`Object:${(p2 - p1).toFixed(3)}ms, Map:${(p3 - p2).toFixed(3)}ms`);}// 内存classTest{}lettest =newTest();test.o = o;test.m = m;复制代码

对于新建元素时的速度表现如下:

我们可以发现新建元素时,Map的速度会快于Object。对于内存使用情况则如下:

通过对比我们可以发现,在拥有一定数量的元素时,Object会比Map占用多了约 78% 的内存。我也进行了多次的测试,发现在拥有足够的元素时,这个百分比是十分稳定的。所以说,在需要进行很多新增操作,且需要储存许多数据的时候,使用Map会更高效。

读取元素时的性能

测试用的代码如下

letn;leto = {}, m =newMap();n =10000;while(n--) { o[Math.random()] =Math.random(); }n =10000;while(n--) { m.set(Math.random(),Math.random()); }letp1 = performance.now();for(keyino) {letk = o[key]; }letp2 = performance.now();for([key]ofm) {letk = m.get(key); }letp3 = performance.now();`Object:${(p2 - p1).toFixed(3)}ms, Map:${(p3 - p2).toFixed(3)}ms`复制代码

对于读取元素时的速度表现如下:

通过对比,我们可以发现Object略占优势,但总体差别不大。

删除元素时的性能

不知道大家是否听说过delete操作符性能低下,甚至有很多时候为了性能,会宁可将值设置为undefined而不使用delete操作符的说法。但其实在v8近来的优化下,它的效率已经提升许多了。

测试用的代码如下

letn;leto = {}, m =newMap();n =10000;while(n--) { o[Math.random()] =Math.random(); }n =10000;while(n--) { m.set(Math.random(),Math.random()); }letp1 = performance.now();for(keyino) {deleteo[key]; }letp2 = performance.now();for([key]ofm) { m.delete(key); }letp3 = performance.now();`Object:${(p2 - p1).toFixed(3)}ms, Map:${(p3 - p2).toFixed(3)}ms`复制代码

对于删除元素时的速度表现如下:

我们可以发现在进行删除操作时,Object的速度会略占优,但整体差别其实也并不大。

特殊情况

其实除了最基本的情况之外,还有一种特殊的情况。还记得我们在前面提到的Object中键的排序吗?我们提到了其中的非负整数会被最先列出。其实对于非负整数作为键的值和其余类型作为键的值来说,v8是会对它们进行区别对待的。负整数作为键的部分会被当成数组对待,即非负整数具有一定的连续性时,会被当成快数组,而过于稀疏时会被当成慢数组。

对于快数组,它拥有连续的内存,所以在进行读写时会更快,且占用更少的内存。更多的内容可以看一下这:探究JS V8引擎下的“数组”底层实现

在键为连续非负整数时,性能如下:

我们可以看到Object不仅平均速度更快了,其占用的内存也大大减少了。

总结

通过对比我们可以发现,Map和Object各有千秋,对于不同的情况下,我们应当作出不同的选择。所以我总结了一下我认为使用Map和Object更为合适的时机。

使用Map:

储存的键不是字符串/数字/或者Symbol时,选择Map,因为Object并不支持

储存大量的数据时,选择Map,因为它占用的内存更小

需要进行许多新增/删除元素的操作时,选择Map,因为速度更快

需要保持插入时的顺序的话,选择Map,因为Object会改变排序

需要迭代/遍历的话,选择Map,因为它默认是可迭代对象,迭代更为便捷

使用Object:

只是简单的数据结构时,选择Object,因为它在数据少的时候占用内存更少,且新建时更为高效

需要用到JSON进行文件传输时,选择Object,因为JSON不默认支持Map

需要对多个键值进行运算时,选择Object,因为句法更为简洁

需要覆盖原型上的键时,选择Object

虽然Map在很多情况下会比Object更为高效,不过Object永远是JS中最基本的引用类型,它的作用也不仅仅是为了储存键值对。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容