这些常见的数组操作,可能导致性能瓶颈


在 JavaScript 中,数组是我们最亲密的“战友”。从数据处理到 UI 渲染,它的身影无处不在。我们每天都在使用 map , filter , reduce 等方法,享受着函数式编程带来的便利和优雅。

然而,优雅的背后可能隐藏着性能的潜在风险。在处理小规模数据时,这些问题微不足道,但当我们的应用需要处理成百上千,甚至数万条数据时,一些看似无害的操作可能会变成压垮骆驼的最后一根稻草,导致页面卡顿、响应迟缓。

1. 不必要的循环和中间数组

这是最常见也最容易被忽视的问题。考虑这样一个场景:我们需要从一个用户列表中,筛选出所有激活状态( isActive
)的用户,并且只提取他们的名字( name )。

很多开发者会这样写:

const users = [/* ... 一个包含 10,000 个用户的数组 ... */];  
  
// 写法一:链式调用  
const activeUserNames = users  
  .filter(user => user.isActive) // 第一次循环,生成一个中间数组  
  .map(user => user.name);        // 第二次循环,在中间数组上操作  
  
console.log(activeUserNames.length);   

这段代码清晰、易读,但它存在一个性能问题: ** 它循环了两次,并创建了一个临时的中间数组 ** 。

  1. filter 方法会遍历所有 10,000 个用户,创建一个新的数组(比如包含 5,000 个激活用户)。
  2. map 方法会再次遍历这个包含 5,000 个用户的中间数组,提取名字,并最终生成结果数组。

总共的迭代次数是 10,000 + 5,000 = 15,000 次。

优化方案:一次循环搞定

我们可以使用 reduce 或者一个简单的 for 循环,只遍历一次数组就完成所有工作。

** 使用 reduce : **

** 使用 for...of (通常性能更好,更易读): **

const activeUserNames = [];  
for (const user of users) {  
  if (user.isActive) {  
    activeUserNames.push(user.name);  
  }  
} // 只循环一次  

这两种优化方法都只进行了 10,000 次迭代,并且没有创建任何不必要的中间数组。在数据量巨大时,性能提升非常显著。

2. unshift shift —— 数组头部的“昂贵”操作

我们需要在数组的开头添加或删除元素时,很自然地会想到 unshift shift 。但这两个操作在性能上非常“昂贵”。

JavaScript 的数组在底层是以 ** 连续的内存空间 ** 存储的。

  • 当我们 unshift 一个新元素时,为了给这个新元素腾出位置,数组中 ** 所有 ** 现有元素都需要向后移动一位。
  • 同样,当我们 shift 删除第一个元素时,为了填补空缺, ** 所有 ** 后续元素都需要向前移动一位。

想象一下在电影院里,我们坐在第一排,然后一个新人要挤到我们左边的 0 号位置。整排的人都得挪动屁股!数据量越大,这个“挪动”的成本就越高。

优化方案:用 push pop ,或者先 reverse

  1. ** 从尾部操作 ** : push pop 只操作数组的末尾,不需要移动其他元素,因此速度极快(O(1) 复杂度)。如果业务逻辑允许,尽量将操作改为在数组尾部进行。
  2. ** 先收集,再反转 ** :如果我们确实需要在开头添加一堆元素,更好的办法是先把它们 push 进一个临时数组,然后通过 concat 或扩展语法合并,或者最后进行一次 reverse
const numbers = [/* ... 100,000 个数字 ... */];  
const newItems = [];  
  
for (let i = 0; i < 1000; i++) {  
  newItems.push(i); // 在新数组尾部添加,非常快  
}  
  
// 最终合并,比 1000 次 unshift 快得多  
const finalArray = newItems.reverse().concat(numbers);  

3. 滥用 includes , indexOf , find

在循环中查找一个元素是否存在于另一个数组中,是一个非常常见的需求。

这段代码的问题在于, filter 每遍历一个库存产品, includes 就要从头到尾搜索 productIds
数组来查找匹配项。如果 productIds 很大,这个嵌套循环的计算量将是 5000 * 1000 ,非常恐怖。

优化方案:使用 Set Map 创建查找表

Set Map 数据结构在查找元素方面具有天然的性能优势。它们的查找时间复杂度接近 O(1),几乎是瞬时的,无论集合有多大。

我们可以先把用于查找的数组转换成一个 Set

const productIds = [/* ... 1,000 个 ID ... */];  
const productsInStock = [/* ... 5,000 个有库存的产品对象 ... */];  
  
// 1. 创建一个 Set 用于快速查找  
const idSet = new Set(productIds); // 这一步很快  
  
// 2. 在 filter 中使用 Set.has()  
const availableProducts = productsInStock.filter(product =>   
  idSet.has(product.id) // .has() 操作近乎瞬时完成  
);  

通过一次性的转换,我们将一个嵌套循环的性能问题,优化成了一个单次循环,性能提升是数量级的。

养成这些小习惯,我们的应用将在面对海量数据时,依然行云流水。