code-review之前端程式碼優化彙總

語言: CN / TW / HK

  • 所謂無規矩不成方圓,前端時間在團隊 code-review 中發現,小編 推薦大家可以大致過一遍,形成自己的編碼習慣

本文主要針對一些 JavaScript 進行優化,使之更加健壯,可讀性更強,更以維護。

if 判斷的優化

1、最簡單的方法:if 判斷

let commodity = {
 phone: '手機',
 computer: '電腦',
 television: '電視',
 gameBoy: '遊戲機',
}
function price(name) {
 if (name === commodity.phone) {
   console.log(1999)
 } else if (name === commodity.computer) {
   console.log(9999)
 } else if (name === commodity.television) {
   console.log(2999)
 } else if (name === commodity.gameBoy) {
   console.log(3999)
 }
}
price('手機') // 9999

缺點:程式碼太長了,維護和閱讀都很不友好

2、好一點的方法:Switch

let commodity = {
 phone: '手機',
 computer: '電腦',
 television: '電視',
 gameBoy: '遊戲機',
}
const price = (name) => {
 switch (name) {
   case commodity.phone:
     console.log(1999)
     break
   case commodity.computer:
     console.log(9999)
     break
   case commodity.television:
     console.log(2999)
     break
   case commodity.gameBoy:
     console.log(3999)
     break
 }
}
price('手機') // 9999

3、更優的方法: 策略模式

  • 策略模式利用組合、委託和多型等技術和思想,可以有效地避免多重條件選擇語句。它提供了對開放—封閉原則的完美支援,將演算法封裝在獨立的 strategy 中,使得它們易於切換,易於理解,易於擴充套件。
const commodity = new Map([
 ['phone', 1999],
 ['computer', 9999],
 ['television', 2999],
 ['gameBoy', 3999],
])
const price = (name) => {
 return commodity.get(name)
}
price('phone') // 1999

includes 的優化

includes 是 ES7 新增的 API,與 indexOf 不同的是 includes 直接返回的是 Boolean 值,indexOf 則 返回的索引值, 陣列和字串都有 includes 方法。

需求:我們來實現一個身份認證方法,通過傳入身份 Id 返回對應的驗證結果

傳統方法

function verifyIdentity(identityId) {
 if (identityId == 1 || identityId == 2 || identityId == 3 || identityId == 4) {
   return '你的身份合法,請通行!'
 } else {
   return '你的身份不合法'
 }
}

includes 優化

function verifyIdentity(identityId) {
 if ([1, 2, 3, 4].includes(identityId)) {
   return '你的身份合法,請通行!'
 } else {
   return '你的身份不合法'
 }
}

for 迴圈

在 JavaScript 中,我們可以使用 for(), while(), for(in),for(of)幾種迴圈,事實上,這三種迴圈中 for(in) 的效率極差,因為他需要查詢雜湊鍵,所以應該儘量少用。

for 迴圈是最傳統的語句,它以變數 i 作為索引,以跟蹤訪問的位置,對陣列進行操作。

var arr = ['a', 'b', 'c']
for (var i = 0; i < arr.length; i++) {
 console.log(arr[i]) //結果依次a,b,c
}

以上的方法有一個問題:就是當陣列的長度到達百萬級時,arr.length 就要計算一百萬次,這是相當耗效能的。所以可以採用以下方法就行改良。

var arr = ['a', 'b', 'c']
for (var i = 0, length = arr.length; i < length; i++) {
 console.log(arr[i]) //結果依次a,b,c
}

此時 arr.length 只需要計算一次,優化了效能。

for-in 一般用來來遍歷物件的屬性的,不過屬性需要 enumerable(可列舉)才能被讀取到。同時 for-in 也可以遍歷陣列,遍歷陣列的時候遍歷的是陣列的下標值。

var obj = { 0: 'a', 1: 'b', 2: 'c' }
for (var key in obj) {
 console.log(key) //結果為依次為0,1,2
}
var arr = ['a', 'b', 'c']
for (var key in a) {
 console.log(key) //結果為依次為0,1,2
}

for-of 語句看著有點像 for-in 語句,但是和 for-of 語句不同的是它不可以迴圈物件,只能迴圈陣列。

var arr = ['a', 'b', 'c']
for (var value of arr) {
 console.log(value) // 結果依次為a,b,c
}

for-of 比 for-in 迴圈遍歷陣列更好。for-of 只要具有 Iterator 介面的資料結構,都可以使用它迭代成員。它直接讀取的是鍵值。for-in 需要窮舉物件的所有屬性,包括自定義的新增的屬性也能遍歷到。且 for-in 的 key 是 String 型別,有轉換過程,開銷比較大。

所以在開發過程中迴圈陣列儘量避免使用 for-in。

陣列去重

陣列去重是實際開發處理資料中經常遇到的,方法有很多,這裡就不一一例舉了。

1、最傳統的方法:利用陣列的 indexOf 下標屬性來查詢。

function unique4(arr) {
 var newArr = []
 for (var i = 0; i < arr.length; i++) {
   if (newArr.indexOf(arr[i]) === -1) {
     newArr.push(arr[i])
   }
 }
 return newArr
}
console.log(unique4([1, 1, 2, 3, 5, 3, 1, 5, 6, 7, 4]))
// [1, 2, 3, 5, 6, 7, 4]

2、優化:利用 ES6 的 Set 方法。

Set 本身是一個建構函式,用來生成 Set 資料結構。Set 函式可以接受一個數組(或者具有 iterable 介面的其他資料結構)作為引數,用來初始化。Set 物件允許你儲存任何型別的值,無論是原始值或者是物件引用。它類似於陣列,但是成員的值都是唯一的,沒有重複的值。

function unique4(arr) {
 return Array.from(new Set(arr)) // 利用Array.from將Set結構轉換成陣列
}
console.log(unique4([1, 1, 2, 3, 5, 3, 1, 5, 6, 7, 4]))
// [1, 2, 3, 5, 6, 7, 4]

箭頭函式

箭頭函式表示式的語法比函式表示式更簡潔。所以在開發中更推薦使用箭頭函式。特別是在 vue 專案中,使用箭頭函式不需要在更 this 重新賦一個變數。

// 使用functions
var arr = [5, 3, 2, 9, 1]
var arrFunc = arr.map(function (x) {
 return x * x
})
console.log(arrFunc)
// 使用箭頭函式
var arr = [5, 3, 2, 9, 1]
var arrFunc = arr.map((x) => x * x)

要注意的是,箭頭函式不繫結 arguments,取而代之用 rest 引數…解決。

// 不能使用 arguments
let fun1 = (b) => {
 console.log(arguments)
}
fun1(2, 92, 32, 32) // Uncaught ReferenceError: arguments is not defined
// 使用rest 引數
let fun2 = (...c) => {
 console.log(c)
}
fun2(3, 82, 32, 11323) // [3, 82, 32, 11323]

Dom 的建立

建立多個 dom 元素時,先將元素 append 到 DocumentFragment 中,最後統一將 DocumentFragment 新增到頁面。

常規方法;

for (var i = 0; i < 1000; i++) {
 var el = document.createElement('p')
 el.innerHTML = i
 document.body.appendChild(el)
}

使用 DocumentFragment 優化多次 append

var frag = document.createDocumentFragment()
for (var i = 0; i < 1000; i++) {
 var el = document.createElement('p')
 el.innerHTML = i
 frag.appendChild(el)
}
document.body.appendChild(frag)

更優的方法:使用一次 innerHTML 賦值代替構建 dom 元素

var html = []
for (var i = 0; i < 1000; i++) {
 html.push('<p>' + i + '</p>')
}
document.body.innerHTML = html.join('')

記憶體洩漏

系統程序不再用到的記憶體,沒有及時釋放,就叫做記憶體洩漏(memory leak)。當記憶體佔用越來越高,輕則影響系統性能,重則導致程序崩潰。

引起記憶體洩漏的原因

全域性變數

1、未宣告變數或者使用 this 建立的變數(this 的指向是 window)都會引起記憶體洩漏

function fn() {
 a = "Actually, I'm a global variable"
}
fn()
function fn() {
 this.a = "Actually, I'm a global variable"
}
fn()

解決方法:

  • 避免建立全域性變數
  • 使用嚴格模式,在 JavaScript 檔案頭部或者函式的頂部加上 use strict。

2、在 vue 單頁面應用,宣告的全域性變數在切換頁面的時候沒有清空

<div id="home">
   這裡是首頁
 </div>
export default {
   mounted() {
     window.test = {
       // 此處在全域性window物件中引用了本頁面的dom物件
       name: 'home',
       node: document.getElementById('home')
     }
   }
 }

解決方案: 在頁面解除安裝的時候順便處理掉該引用。

destroyed () {
 window.test = null // 頁面解除安裝的時候解除引用
}

閉包

閉包引起的記憶體洩漏原因:閉包可以維持函式內區域性變數,使其得不到釋放。

function fn() {
 var a = "I'm a"
 return function () {
   console.log(a)
 }
}

解決:將事件處理函式定義在外部,解除閉包,或者在定義事件處理函式的外部函式中,刪除對 dom 的引用。

定時器或事件監聽

由於專案中有些頁面難免會碰到需要定時器或者事件監聽。但是在離開當前頁面的時候,定時器如果不及時合理地清除,會造成業務邏輯混亂甚至應用卡死的情況,這個時就需要清除定時器事件監聽,即在頁面解除安裝(關閉)的生命週期函式裡,清除定時器。

methods:{
 resizeFun () {
   this.tableHeight = window.innerHeight - document.getElementById('table').offsetTop - 128
 },
 setTimer() {
   this.timer = setInterval(() => { })
 },
 clearTimer() {//清除定時器
 clearInterval(this.timer)
   this.timer = null
}
},
mounted() {
 this.setTimer()
 window.addEventListener('resize', this.resizeFun)
},
beforeDestroy() {
 window.removeEventListener('resize', this.resizeFun)
 this.clearTimer()
}

防抖與節流

在前端開發的過程中,我們經常會需要繫結一些持續觸發的事件,如 resize、scroll、mousemove 等等,但有些時候我們並不希望在事件持續觸發的過程中那麼頻繁地去執行函式。這時候就用到防抖與節流。

案例 1:遠端搜尋時需要通過介面動態的獲取資料,若是每次使用者輸入都介面請求,是浪費頻寬和效能的。

<Select :remote-method="remoteMethod">
   <Option v-for="item in temoteList" :value="item.value" :key="item.id">{{item.label}}</Option>
</Select>
<script>
function debounce(fn, wait) {
 let timeout = null
 return function () {
   if (timeout !== null) clearTimeout(timeout)
   timeout = setTimeout(fn, wait)
 }
}
export default {
 methods:{
   remoteMethod:debounce(function (query) {
       // to do ...
   }, 200),
 }
}
<script>

案例 2:持續觸發 scroll 事件時,並不立即執行 handle 函式,當 1000 毫秒內沒有觸發 scroll 事件時,才會延時觸發一次 handle 函式。

function debounce(fn, wait) {
 let timeout = null
 return function () {
   if (timeout !== null) clearTimeout(timeout)
   timeout = setTimeout(fn, wait)
 }
}
function handle() {
 console.log(Math.random())
}
window.addEventListener('scroll', debounce(handle, 1000))

非同步載入 js

預設情況下,瀏覽器是同步載入 js 指令碼,解析 html 過程中,遇到 <script> 標籤就會停下來,等指令碼下載、解析、執行完後,再繼續向下解析渲染。

如果 js 檔案體積比較大,下載時間就會很長,容易造成瀏覽器堵塞,瀏覽器頁面會呈現出“白屏”效果,使用者會感覺瀏覽器“卡死了”,沒有響應。此時,我們可以讓 js 指令碼非同步載入、執行。

<script src="path/to/home.js" defer></script>
<script src="path/to/home.js" async></script>

上面程式碼中,<script> 標籤分別有 defer 和 async 屬性,瀏覽器識別到這 2 個屬性時 js 就會非同步載入。也就是說,瀏覽器不會等待這個指令碼下載、執行完畢後再向後執行,而是直接繼續向後執行。

defer 與 async 區別:

  • defer:DOM 結構完全生成,以及其他指令碼執行完成,才會執行(渲染完再執行)。有多個 defer 指令碼時,會按照頁面出現的順序依次載入、執行。
  • async:一旦下載完成,渲染引擎就會中斷渲染,執行這個指令碼以後,再繼續渲染(下載完就執行)。有多個 async 指令碼時,不能保證按照頁面出現順序載入、執行。