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 ? '所有 id 都是唯一的' : `不唯一的記錄 ${size - set.size}`
)

沒錯,其實就是在檢驗 uid 函數,看生成的 id 是否唯一。有趣的是,這裡是如何利用 Set 來進行唯一性判斷的, 為什麼 set.size === size 就可以判斷創建的 uuid 是否是唯一的呢?

正是因為 Set 集合中的值只能出現一次,因此當使用 map 並使用 uid() 生成值時,如果有一樣的值,是不會被加入到集合當中的。利用這個特性,就可以在最開始的例子中統計,是否有重複的值,以及重複的個數:

  • 調用 set.size, 如果所有的都是唯一的,則 map 後的集合長度和最開始定義的 size 長度一致;
  • 否則,sizeset.size 的差異就是重複的個數。

合理使用 Set 不僅可以使代碼更高效,也可以讓代碼更加精煉和易於理解,因此在遇到類似去重、統計、計數等場景,如果之前毫不猶豫和考慮使用數組的,可以多留意下,是否可以使用 Set 來提高代碼效率。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。