這些常見的數組操作,可能導致性能瓶頸
在 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);
這段代碼清晰、易讀,但它存在一個性能問題:它循環了兩次,并創建了一個臨時的中間數組。
- filter 方法會遍歷所有 10,000 個用戶,創建一個新的數組(比如包含 5,000 個激活用戶)。
- 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
從尾部操作:push 和 pop 只操作數組的末尾,不需要移動其他元素,因此速度極快(O(1) 復雜度)。如果業務邏輯允許,盡量將操作改為在數組尾部進行。
先收集,再反轉:如果我們確實需要在開頭添加一堆元素,更好的辦法是先把它們 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() 操作近乎瞬時完成
);
通過一次性的轉換,我們將一個嵌套循環的性能問題,優化成了一個單次循環,性能提升是數量級的。
養成這些小習慣,我們的應用將在面對海量數據時,依然行云流水。