资讯专栏INFORMATION COLUMN

如何利用JavaScript的Map提升性能

notebin / 3612人阅读

摘要:测试结果展示了使用的性能收益,尤其是添加和删除键值的时。这个结果着实令人震惊,但是没有循环,添加属性的性能胜过常规对象。

在ES6中引入JavaScript的新特性中,我们看到了SetMap的介绍。与常规对象和Array不同的是,它们是“键控集合(keyed collections)”。这就是说它们的行为有稍许不同,并且在特定的上下文中使用,它们可以提供相当大的性能优势。

在这篇文章中,我将剖析Map,它究竟有何不同,哪里可以派上用场,相比于常规对象有什么性能优势。

Map与常规对象有什么不同

Map和常规对象主要有2个不同之处。

1.无限制的键(Key)

常规JavaScript对象的键必须是StringSymbol,下面的对象说明的这一点:

const symbol = Symbol();
const string2 = "string2";

const regularObject = {
  string1: "value1",
  [string2]: "value2",
  [symbol]: "value3"
};

相比之下,Map允许你使用函数、对象和其它简单的类型(包括NaN)作为键,如下代码:

const func = () => null;
const object = {};
const array = [];
const bool = false;
const map = new Map();

map.set(func, "value1");
map.set(object, "value2");
map.set(array, "value3");
map.set(bool, "value4");
map.set(NaN, "value5");

在链接不同数据类型时,这个特性提供了极大的灵活性。

2.直接遍历

在常规对象中,为了遍历keys、values和entries,你必须将它们转换为数组,如使用Object.keys()Object.values()Object.entries(),或者使用for ... in循环,因为常规对象不能直接遍历,另外for ... in循环还有一些限制:它仅仅遍历可枚举属性、非Symbol属性,并且遍历的顺序是任意的。

Map可以直接遍历,并且由于它是键控集合,遍历的顺序和插入键值的顺序是一致的。你可以使用for ... of循环或forEach方法来遍历Map的entries,如下代码:

for (let [key, value] of map) {
  console.log(key);
  console.log(value);
};
map.forEach((key, value) => {
  console.log(key);
  console.log(value);
});

还有一个好处就是,你可以调用map.size属性来获取键值数量,而对于常规对象,为了做到这样你必须先转换为数组,然后获取数组长度,如:Object.keys({}).length

MapSet有何不同

Map的行为和Set非常相似,并且它们都包含一些相同的方法,包括:has、get、set、delete。它们两者都是键控集合,就是说你可以使用像forEach的方法来遍历元素,顺序是按照插入键值排列的。

最大的不同是Map通过键值(key/value)成对出现,就像你可以把一个数组转换为Set,你也可以把二维数组转换为Map

const set = new Set([1, 2, 3, 4]);
const map = new Map([["one", 1], ["two", 2], ["three", 3], ["four", 4]]);
类型转换

要将Map切换回数组,你可以使用ES6的结构语法:

const map = new Map([["one", 1], ["two", 2]]);
const arr = [...map];

到目前为止,将Map与常规对象的互相转换依然不是很方便,所以你可能需要依赖一个函数方法,如下:

const mapToObj = map => {
  const obj = {};
  map.forEach((key, value) => { obj[key] = value });
  return obj;
};
const objToMap = obj => {
  const map = new Map();
  Object.keys(obj).forEach(key => { map.set(key, obj[key]) });
  return map;
};

但是现在,在八月份ES2019的首次展示中,我们看见了Object引入了2个新方法:Object.entries()Object.fromEntries(),这可以使上述方法简化许多:

const obj2 = Object.fromEntries(map);
const map2 = new Map(Object.entries(obj));

在你使用Object.fromEntries转换map为object之前,确保map的key在转换为字符串时会产生唯一的结果,否则你将面临数据丢失的风险。

性能测试

为了准备测试,我会创建一个对象和一个map,它们都有1000000个相同的键值。

let obj = {}, map = new Map(), n = 1000000;
for (let i = 0; i < n; i++) {
  obj[i] = i;
  map.set(i, i);
}

然后我使用console.time()来衡量测试,由于我特定的系统和Node.js版本的原因,时间精度可能会有波动。测试结果展示了使用Map的性能收益,尤其是添加和删除键值的时。

查询

let result;
console.time("Object");
result = obj.hasOwnProperty("999999");
console.timeEnd("Object");
// Object: 0.250ms

console.time("Map");
result = map.has(999999);
console.timeEnd("Map");
// Map: 0.095ms (2.6 times faster)

添加

console.time("Object");
obj[n] = n;
console.timeEnd("Object");
// Object: 0.229ms

console.time("Map");
map.set(n, n);
console.timeEnd("Map");
// Map: 0.005ms (45.8 times faster!)

删除

console.time("Object");
delete obj[n];
console.timeEnd("Object");
// Object: 0.376ms

console.time("Map");
map.delete(n);
console.timeEnd("Map");
// Map: 0.012ms (31 times faster!)
Map在什么情况下更慢

在测试中,我发现一种情况常规对象的性能更好:使用for循环去创建常规对象和map。这个结果着实令人震惊,但是没有for循环,map添加属性的性能胜过常规对象。

console.time("Object");
for (let i = 0; i < n; i++) {
  obj[i] = i;
}
console.timeEnd("Object");
// Object: 32.143ms

let obj = {}, map = new Map(), n = 1000000;
console.time("Map");
for (let i = 0; i < n; i++) {
  map.set(i, i);
}
console.timeEnd("Map");
// Map: 163.828ms (5 times slower)
举个例子

最后,让我们看一个Map比常规对象更合适的例子,比如说我们想写一个函数去检查2个字符串是否由相同的字符串随机排序。

console.log(isAnagram("anagram", "gramana")); // Should return true
console.log(isAnagram("anagram", "margnna")); // Should return false

有许多方法可以做到,但是这里,map可以帮忙我们创建一个最简单、最快速的解决方案:

const isAnagram = (str1, str2) => {
  if (str1.length !== str2.length) {
    return false;
  }
  const map = new Map();
  for (let char of str1) {
    const count = map.has(char) ? map.get(char) + 1 : 1;
    map.set(char, count);
  }
  for (let char of str2) {
    if (!map.has(char)) {
      return false;
    }
    const count = map.get(char) - 1;
    if (count === 0) {
      map.delete(char);
      continue;
    }
    map.set(char, count);
  }
  return map.size === 0;
};

在这个例子中,当涉及到动态添加和删除键值,无法提前确认数据结构(或者说键值的数量)时,map比object更合适。

我希望这篇文章对你有所帮助,如果你之前没有使用过Map,不妨开阔你的眼界,衡量现代JavaScript的价值体现。

译者注:我个人不太同意作者的观点,从以上的描述来看,Map更像是以空间为代价,换取速度上的提升。那么对于空间和速度的衡量,必然存在一个阈值。在数据量比较少时,相比与速度的提升,其牺牲的空间代价更大,此时显然是不适合使用Map;当数据量足够大时,此时空间的代价影响更小。所以,看开发者如何衡量两者之间的关系,选择最优解。

原文链接

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106726.html

相关文章

  • 前端每周清单第 29 期:Web 现状分析与优化策略、Vue 单元测试、Headless Chrom

    摘要:前端每周清单第期现状分析与优化策略单元测试爬虫作者王下邀月熊编辑徐川前端每周清单专注前端领域内容,以对外文资料的搜集为主,帮助开发者了解一周前端热点分为新闻热点开发教程工程实践深度阅读开源项目巅峰人生等栏目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清单第 29 期:Web 现状分析与优化策略...

    HackerShell 评论0 收藏0
  • The Cost Of JavaScript 2018 精读

    摘要:目前我们的业务项目采用的来进行优化和首屏性能提升。可变性需要让开发人员降低开发时的基准线,来保证每一个用户的体验。对于路由的切分以及库的引入来说,这一个原则至关重要。快速生成一份站点的性能审查报告。 The Cost Of JavaScript 2018 关于原文 原文是在Medium上面看到的,Chrome工程师Addy Osmani发布的一篇文章,这位的Medium上面的自我介绍里...

    lushan 评论0 收藏0
  • 前端每周清单第 49 期:Webpack 4 Beta 尝鲜,React Windowing 与 s

    摘要:尽管等待了多年,但是最终还是发布了正式版本与上一个版本相比未有重大变化,主要着眼于部分错误修复与提升。能够将异步函数移入独立线程中,可以看做函数的单函数简化版。不过需要注意的是,仅支持纯函数,其会在独立的作用域中运行这些函数。 showImg(https://segmentfault.com/img/remote/1460000013038757); 前端每周清单专注前端领域内容,以对...

    muzhuyu 评论0 收藏0
  • 前端面试题(4)JavaScript

    摘要:变量声明提升在中,函数声明与变量声明经常被引擎隐式地提升到当前作用域的顶部。对象的方法和属性是在全局范围内有效的。未形成标准,实现混乱。 前端面试题JavaScript(一) JavaScript的组成 JavaScript 由以下三部分组成: ECMAScript(核心):JavaScript 语言基础 DOM(文档对象模型):规定了访问HTML和XML的接口 BOM(浏览器对...

    1treeS 评论0 收藏0
  • 前端面试题(4)JavaScript

    摘要:变量声明提升在中,函数声明与变量声明经常被引擎隐式地提升到当前作用域的顶部。对象的方法和属性是在全局范围内有效的。未形成标准,实现混乱。 前端面试题JavaScript(一) JavaScript的组成 JavaScript 由以下三部分组成: ECMAScript(核心):JavaScript 语言基础 DOM(文档对象模型):规定了访问HTML和XML的接口 BOM(浏览器对...

    spademan 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<