Vincent Ko

VK's Blog

性能提升技巧:使用Set和数组方法优化JavaScript代码

在编写和维护前端代码的过程中,我们常常会遇到需要进行性能优化的场景。尤其是在处理数组和对象的时候,恰当地使用 JavaScript 提供的方法不仅能提升代码的执行效率,还能让代码更加简洁易懂。在本文中,将从一个实际业务代码的 Review 中,窥探如何通过合理利用 JavaScript 的数组和集合方法来达到优化的目的。

业务场景#

让我们先来看一个常见的场景,我们需要验证用户是否已经为每个选中的权限范围分配了相应的权限。这个需求听起来非常直接,但是在实际实现时,可能会涉及到一系列的数组操作,这就是我们今天要优化的源代码:

// 初始的权限校验逻辑
const selectedObjItem = [];
this.selectedPermissions.forEach((item) => {
  !selectedObjItem.includes(item.action_id) && selectedObjItem.push(item.action_id);
});

const result = this.selectedScopes.every(scope => {
  if (scope.has_instance) {
    return selectedObjItem.includes(scope.action_id);
  }
  return true;
})

if (!result) {
  showMsg(this.$t('AUTH:请选择对象范围'), 'warning');
  return false;
}

这段代码的目的是校验是否每个已选场景 (selectedScopes) 都选择了权限范围,且这些权限范围需要在 selectedPermissions 中找到匹配(每个权限对象具有唯一标志 action_id)。

代码虽然表面上能达到预期效果,但经过细致分析,我们会发现其实还存在着优化的空间。事实上,我们可以通过几个简单的改动来提升执行效率并使代码更加清晰。为此,可以考虑以下几个问题:

  1. 要实现数组记录和去重,是否有更高效的方式?
  2. every 是否是最好遍历、验证选择?
  3. 最后的结果判断,一定要等到所有遍历结束吗?

优化#

1. 去重与记录#

第一步的记录其实也包含了去重,主要在于 push 前的证据判定: !selectedObjItem.includes(item.action_id)。只要涉及去重,其实就可以想到使用 ES6 天然、高效的数据类型 Set,因为 Set 本身就具有去重属性,因此可以代码优化为:

const selectedObjItem = new Set();
this.selectedPermissions.forEach((item) => {
  selectedObjItem.add(item.action_id);
});

使用 Set 去重并配合 has 方法检查元素是否存在,通常比使用数组去重(通过 push 配合 includes 检查)的方式更加高效。原因在于:

  1. 时间复杂度
    • Setadd 方法与 has 方法的时间复杂度通常是常数时间复杂度 O (1),这是因为 Set 内部是通过哈希表来实现的,这允许快速地查找和插入数据。
    • 数组的 includes 方法时间复杂度为 O (n),因为在最坏的情况下它需要遍历整个数组来判断某个元素是否存在。
  2. 去重
    • Set 数据结构在设计上就是一组无重复元素的集合,它自动管理元素的唯一性。当你尝试添加一个已经存在于 Set 中的元素时,Set 将不会进行任何操作,因此无需手动编写去重逻辑。
    • 在数组中去重,则必须要编写额外的逻辑(通常是使用 includes 检查后再 push)来保证添加的元素是唯一的,这增加了代码复杂性和运行的操作步骤。
  3. 代码简洁性
    • 使用 Set 会使得代码更为简洁和易于理解。因为 Set 的接口意图明确,代表了集合这一数据结构,使得代码的意图更加清晰、语义化更好;
    • 使用数组去重涉及的 includespush 方法,虽然它们都是数组方法且易于理解,但需要结合使用来实现去重,代码看起来不够直观。

关于 Set 的使用和应用,在 [下文](## 附:Set 的说明与用例) 加以介绍。

2. 检测优化#

源代码中使用 every 方法是可以的,但同时又是低效的,因为 every 需要遍历所有项,而本次的校验目的是所有项目均选择,所以其实只要有一个没有选择,就可以提前返回错误结果,而没有必要去检查后面的内容了。

有了这样的思路,很容易就想起 some 方法,优化代码如下:

const result = this.selectedScopes.some(scope => {
  if (scope.has_instance) {
    return !selectedObjItem.has(scope.action_id);
  }
  return false;
});

看起来代码逻辑和结构与原来无异,但实际上效率却会提高,就在于 some 在返回 true 后,不会继续后续的循环。

3. 完整代码#

优化后的完整代码如下:

// 去重记录 action_id
const selectedObjItem = new Set();
this.selectedPermissions.forEach((item) => {
  selectedObjItem.add(item.action_id);
});

// 检测是否所有选项都已选中
const result = this.selectedScopes.some(scope => {
  if (scope.has_instance) {
    return !selectedObjItem.has(scope.action_id);
  }
  return false;
});

if (result) {
  showMsg(this.$t('AUTH:请选择对象范围'), 'warning');
  return false;
}

场景训练#

类似的,看这样一个示例:

已知 batchAddSource 是一个二维数组,其值为: [['a','b'], ['c','d'], ...]selectedListbatchAddSource 的子集,表示已选内容,现需要一个方法 getUnselectedList,传入已选内容,返回未选内容。

思路#

本问题的核心还是去重问题,结合前面的优化思路,可以有以下想法:

  1. 二维数组比较,可以将内层数组转换成更好去比较的字符串类型 (join 方法)
  2. 使用 Set 数据结构的 has 方法判断是否存在
const getUnselectedList = (selectedList, batchAddSource) => {
	const selectedSource = new Set(selectedList.map(item => item.join('.')))
	return batchAddSource.filter(item => !selectedSource.has(item.join('.')))
}

总结#

JavaScript 是一个不断发展的语言, ES6 版本引入了新的语法和特性,极大地提高了代码编写的效率和可读性。这里简单总结下提到的几个方法和应用

  1. 使用新的数据结构Set:实现数组去重和高效记录非重复值的功能非常方便。相比传统的遍历检查方法,Set 的原生机制保证了集合中的唯一性,对性能优化起着显著的作用,尤其在处理大量数据时能显著提升效率。
  2. 合理使用数组方法: ES6 提供了如 forEacheverysome 等数组方法,合理使用这些方法可以让代码更清晰,也能根据不同的业务逻辑需求选择最适应的迭代方式,如使用 some 来提前返回结果,以避免不必要的遍历。
  3. 优化逻辑判断: 通过逻辑上的优化,如及早退出循环,可以避免执行不必要的计算,从而节省资源和时间。在实现数组或集合的校验操作时,考虑边界条件和短路逻辑可以做到更高效的代码执行。
  4. 利用 ES6 的简洁语法: 如箭头函数、模板字符串等新特性,可以让代码更简洁,避免冗余代码,并且提高代码的易理解性。

在实际编程实践中,每个优化点虽然可能只带来微小的性能提升,但整体上,这些改进加起来能显著提高应用程序的性能和用户体验。综合上述优化手段,我们能编写出既高效又具备良好可读性的 JavaScript 代码。

附:Set 的说明与用例#

Set 是一个特殊的类型集合 —— “值的集合”(没有键),它的每一个值只能出现一次

上文已经介绍了 Set 的一个应用场景,这里现对该数据类型的特性进行一个简单回顾。它的主要方法如下:

  • new Set(iterable) —— 创建一个 set,如果提供了一个 iterable 对象(通常是数组),将会从数组里面复制值到 set 中。
  • set.add(value) —— 添加一个值,返回 set 本身
  • set.delete(value) —— 删除值,如果 value 在这个方法调用的时候存在则返回 true ,否则返回 false
  • set.has(value) —— 如果 value 在 set 中,返回 true,否则返回 false
  • set.clear() —— 清空 set。
  • set.size —— 返回元素个数。

它的主要特点是,重复使用同一个值调用 set.add(value) 并不会发生什么改变。这就是 Set 里面的每一个值只出现一次的原因。

计数(统计)#

先看以下代码,在不给注释的情况下是否能理解在做什么,以及相关原理:

const uid = () =>
  String(
    Date.now().toString(32) +
      Math.random().toString(16)
  ).replace(/\./g, '')

const size = 1000000
const set = new Set(new Array(size)
  .fill(0)
  .map(() => uid()))

console.log(
  size === set.size ? 'all ids are unique' : `not unique records ${size - set.size}`
)

没错,其实就是在检验 uid 函数,看生成的 id 是否唯一。有趣的是,这里是如何利用 Set 来进行唯一性判断的, 为什么 set.size === size 就可以判断创建的 uuid 是否是唯一的呢?

正是因为 Set 集合中的值只能出现一次,因此当使用 map 并使用 uid() 生成值时,如果有一样的值,是不会被加入到集合当中的。利用这个特性,就可以在最开始的例子中统计,是否有重复的值,以及重复的个数:

  • 调用 set.size, 如果所有的都是唯一的,则 map 后的集合长度和最开始定义的 size 长度一致;
  • 否则,sizeset.size 的差异就是重复的个数。

合理使用 Set 不仅可以使代码更高效,也可以让代码更加精炼和易于理解,因此在遇到类似去重、统计、计数等场景,如果之前毫不犹豫和考虑使用数组的,可以多留意下,是否可以使用 Set 来提高代码效率。

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。