javascript 序列化 json-JS项目中应该使用Object还是Map? |项目评审

前言

在日常的JavaScript项目中,最常用的数据结构就是各种方式的通配符对格式(键值对)。 在JavaScript中,不仅最基本的Object是这种格式,ES6中新添加的Map也是通配符对格式。 它们的用法在许多情况下非常相似。 不知道有没有人和我一样苦恼该选哪一个呢? 在本菜最近的项目中,我又遇到了这种麻烦,所以我就一直尝试比较应该使用哪个。

本文将讲解Object和Map的区别javascript 序列化 json,并从多个角度对Object和Map进行比较:

希望大家看完这篇文章后,能够在以后的项目中做出更加合适的选择。

使用对比

对于Object,其key的类型只能是string、number或者Symbol; 对于Map,它可以是任何类型。 (包括日期、地图或自定义对象)

Map中的元素将保持它们插入时的顺序; 而Object不会完全保持其插入的顺序,而是会按照以下规则进行排序:

读取Map的厚度非常简单,只需调用它的.size()方法即可; 读取对象的宽度时需要额外的估计:Object.keys(obj).length

Map是一个可迭代对象,因此可以通过forof循环或.foreach()来迭代其中的通配符对; 而普通对象通配符对默认是不可迭代的,只能通过forin循环访问(或者使用Object.keys(o)、Object.values(o)、Object.entries(o)获取代表key的数字)或 value) 迭代时的顺序就是前面提到的顺序。

const o = {};
const m = new Map();
o[Symbol.iterator] !== undefined// false
m[Symbol.iterator] !== undefined// true

当一个key被添加到Map中时,其原型上的key不会被覆盖; 当将键添加到对象时,其原型上的键可能会被覆盖:

Object.prototype.x = 1;
const o = {x:2};
const m = new Map([[x,2]]);
o.x; // 2,x = 1 被覆盖了
m.x; // 1,x = 1 不会被覆盖

JSON 默认支持 Object,但不支持 Map。 如果想通过JSON传输Map,需要使用.toJSON()方法,然后在JSON.parse()中传入恢复函数进行恢复。

对于JSON,这里就不具体展开了。 有兴趣的同学可以看看这个:JSON的序列化与解析

const o = {x:1};
const m = new Map([['x'1]]);
const o2 = JSON.parse(JSON.stringify(o)); // {x:1}
const m2 = JSON.parse(JSON.stringify(m)) // {}

复合句对比造句的差异

目的

const o = {}; // 对象字面量
const o = new Object(); // 调用构造函数
const o = Object.create(null); // 调用静态方法 Object.create 

对于对象,95%以上的情况下我们仍然选择对象字面量。 它不仅编写起来最简单,而且与下面的函数调用相比,它在性能方面更加高效。 对于创建函数,唯一可能的用例是显式封装基本类型; 而Object.create可以设置对象的原型。

地图

const m = new Map(); // 调用构造函数

与Object不同的是javascript 序列化 json,Map没有那么多花哨的创建方法,一般只使用它的构造函数来创建。

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

添加/读取/删除元素之间的区别

目的

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

对于添加元素,使用第一种方法似乎更简单,但它也有一些局限性:

有关条件属性访问表达式的更多信息,可以看看这个:条件属性访问表达式

地图

const m = new Map();
//新增/修改
m.set('x'1);
//读取
map.get('x');
//删除
map.delete('b');

javascript 序列化 json_java反序列化_json序列化

对于简单的增删改查,Map上的方法使用起来也很方便; 但在进行联动操作时,Map中的使用会稍显臃肿:

const m = new Map([['x',1]]);
// 若想要将 x 的值在原有基础上加一,我们需要这么做:
m.set('x', m.get('x') + 1);
m.get('x'); // 2

const o = {x1};
// 在对象上修改则会简单许多:
o.x++;
o.x // 2

性能比较

接下来我们讨论一下Object和Map的性能。 不知道大家有没有听说过Map的性能比Object要好。 我已经看过很多次了,甚至在JS高中就提到过Map相对于Object的性能优势; 但性能的概括性非常好,所以我要做一些测试来看看差异。

测试方法

我这里进行的性能测试都是基于v8引擎的。 速度将由JS标准库自带的performance.now()函数决定,内存使用情况将由Chromedevtool中的内存检查。

对于速度测试,由于单次操作的速度太快,很多情况下performance.now()会返回0。 所以我判断了10000次循环后的时间差。 由于循环本身也会抢占部分时间,所以下面的测试只能作为一个粗略的参考。

创建时的性能

用于测试的代码如下:

let n,  n2 = 5;
// 速度
while (n2--) {
  let p1 = performance.now();
  n = 10000;
  while (n--) { let o = {}; }
  let p2 = performance.now();
  n = 10000;
  while (n--) { let m = new Map(); }
  let p3 = performance.now();
  console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 内存
class Test {}
let test = new Test();
test.o = o;
test.m = m;

首先要比较的是创建Object和Map时的性能。 创建速率的行为如下:

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

javascript 序列化 json_java反序列化_json序列化

我们主要关注它的RetainedSize,它代表为其分配的空间。 (即删除时释放的显存大小)

通过比较,我们可以发现空的Object比空的Map占用的内存要少。 所以Object赢了这一轮。

添加元素时的性能

用于测试的代码如下:

console.clear();
let n,  n2 = 5;
let o = {}, m = new Map();
// 速度
while (n2--) {
  let p1 = performance.now();
  n = 10000;
  while (n--) { o[Math.random()] = Math.random(); }
  let p2 = performance.now();
  n = 10000;
  while (n--) { m.set(Math.random(), Math.random()); }
  let p3 = performance.now();
  console.log(`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`);
}
// 内存
class Test {}
let test = new Test();
test.o = o;
test.m = m;

新元素的倍率性能如下:

我们可以发现,在创建新元素时,Map会比Object更快。 内存使用情况如下:

通过对比我们可以发现,当元素达到一定数量时,Object会比Map多占用78%左右的显存。 我也进行了多次测试,发现当元素足够多的时候这个比例是非常稳定的。 因此,当需要进行很多新的操作、需要存储大量的数据时,使用Map会更加高效。

读取元素时的性能

用于测试的代码如下:

let n;
let o = {}, m = new Map();

n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }

let p1 = performance.now();
for (key in o) { let k = o[key]; }
let p2 = performance.now();
for ([key] of m) { let k = m.get(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`

读取元素的速率如下:

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

删除元素时的性能

不知道大家有没有听说过,delete操作符的性能很低,甚至很多时候为了性能,宁愿将值设置为undefined,也不愿意使用delete操作符。 但看来在v8最近的优化下,其效率提升了不少。

用于测试的代码如下:

let n;
let o = {}, m = new Map();

n = 10000;
while (n--) { o[Math.random()] = Math.random(); }
n = 10000;
while (n--) { m.set(Math.random(), Math.random()); }

let p1 = performance.now();
for (key in o) { delete o[key]; }
let p2 = performance.now();
for ([key] of m) { m.delete(key); }
let p3 = performance.now();
`Object: ${(p2 - p1).toFixed(3)}ms, Map: ${(p3 - p2).toFixed(3)}ms`

对于删除元素的速率,性能如下:

我们可以发现,Map在执行删除操作时速度稍好一些,但整体差异似乎并不大。

特例

虽然不只是最基本的情况,但也有特殊情况。 还记得我们后面提到的Object中key的排序吗? 我们讨论了首先枚举的非负整数。 虽然对于非负整数的值作为key和其他类型的值作为key,v8会区别对待。 负整数作为key的部分会被视为链表,即当非负整数有一定的连续性时,会被视为快速链表,当过于稀疏时,会被视为快速链表。可以看作是一个慢速链表。

对于快速链表来说,它有连续的显存,所以读写的时候会更快,而且占用显存也更少。更多内容可以看一下这个:探索“数组”底层实现” 在 JSV8 发动机下

当key为连续非负整数时,性能如下:

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

总结

通过比较我们可以发现Map和Object各有优缺点,不同的情况我们应该做出不同的选择。 所以我总结了一下我觉得用Map和Object比较合适的时候。

使用地图:

使用对象:

虽然Map在很多情况下比Object更高效,但Object始终是JS中最基本的引用类型,它的作用不仅仅是存储通配符对。

参考

探究JSV8引擎下“数组”的底层实现

V8 中的快速属性

浅层尺寸、保留尺寸和深尺寸

V8 中 JS 中对象属性的缓慢删除

ES6 — 映射与对象 — 什么以及何时?

JavaScript 中级编程(第四版)

JavaScript:权威指南(第七版)