JavaScript 高階技巧

SRIGT發表於2024-03-12

0x01 深淺複製

  • 開發中經常需要複製(複製)一個物件,如果直接賦值,則對複製物件的修改會影響到源物件

    const o1 = {
      a: 1,
      b: 2
    }
    const o2 = o1
    console.log(o2) // { a: 1, b: 2 }
    
    o2.a = 3
    console.log(o1) // { a: 3, b: 2 }
    console.log(o2) // { a: 3, b: 2 }
    

    原因在於,直接賦值的方法是在複製物件資料在棧中的地址,即兩個變數操作同一個位置的資料

  • 深複製與淺複製只針對引用型別

(1)淺複製

  • 淺複製只將物件或陣列的第一層進行復制,其他層級複製的是所儲存的記憶體地址

a. 物件

  • 方法:Object.assign(to, from){...obj}

  • 案例:

    const o1 = {
      a: 1,
      b: 2
    }
    
    const o2 = {}
    Object.assign(o2, o1)
    console.log(o2) // { a: 1, b: 2 }
    o2.a = 3
    console.log(o1) // { a: 1, b: 2 }
    console.log(o2) // { a: 3, b: 2 }
    
    const o3 = {...o1}
    console.log(o3) // { a: 1, b: 2 }
    
  • 淺複製僅複製第一層資料,而不會深入

    const o1 = {
      a: 1,
      b: {
        c: 2
      }
    }
    
    const o2 = {}
    Object.assign(o2, o1)
    
    o2.b.c = 3
    
    console.log(o1) // { a: 1, b: { c: 3 } }
    console.log(o2) // { a: 1, b: { c: 3 } }
    

b. 陣列

  • 方法:Array.prototype.concat()[...array]

  • 案例:

    const a1 = [1, 2, 3]
    
    const a2 = a1.concat([])
    console.log(a2) // [ 1, 2, 3 ]
    
    const a3 = [...a1]
    console.log(a3) // [ 1, 2, 3 ]
    

(2)深複製

  • 深複製會構造一個新的複合陣列或物件,遇到引用所指向的引用資料型別會繼續執行複製

  • 常見方法:

a. 遞迴方法

  • 遞回應用舉例:透過 setTimeout 模擬 setInterval 效果實現頁面中的時鐘每秒重新整理

    document.body.appendChild(document.createElement('div'))
    function getTime() {
    document.querySelector('div').innerHTML = new Date().toLocaleString()
    setTimeout(getTime, 1000)
    }
    getTime()
    
  • 基於遞迴方法的深複製案例:

    const o1 = {
      a: 1,
      b: {
        c: 2
      }
    }
    const o2 = {}
    
    function deepCopy(newObj, oldObj) {
      // 遍歷oldObj中的所有屬性
      for (let key in oldObj) {
        // 如果屬性值是物件,則遞迴進行深度複製
        if (typeof oldObj[key] === 'object') {
          newObj[key] = {} // 為newObj建立一個空物件作為屬性值
          deepCopy(newObj[key], oldObj[key]) // 遞迴呼叫deepCopy函式複製屬性值中的所有屬性
        } else {
          // 如果屬性值不是物件,直接複製
          newObj[key] = oldObj[key]
        }
      }
    }
    
    deepCopy(o2, o1)
    console.log(o2)	// { a: 1, b: { c: 2 } }
    o2.b.c = 3
    console.log(o1)	// { a: 1, b: { c: 2 } }
    console.log(o2)	// { a: 1, b: { c: 3 } }
    

b. Lodash

  • Lodash 是第三方 JS 庫,官網連結
  • 引入 Lodash:https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js
const o1 = {
  a: 1,
  b: {
    c: 2
  }
}

const o2 = _.cloneDeep(o1)

console.log(o2)

c. JSON 方法

const o1 = {
  a: 1,
  b: {
    c: 2
  }
}

const o2 = JSON.parse(JSON.stringify(o1))

console.log(o2)

0x02 異常處理

  • 定義:預估程式碼執行過程中可能發生的錯誤,使用特定的方法對這些錯誤進行合適的處理
  • 意義:有助於提高程式碼健壯性

(1)throw 丟擲異常

function division(x, y) {
  if(!x || !y) {
    throw "The parameter cannot be empty"
  }
  if(y === 0) {
    throw new Error("Divisor cannot be zero")
  }
}
division()
  • throw 丟擲異常資訊,程式也會中止
  • Error 物件常配合 throw 使用,能夠設定更詳細的錯誤訊息

(2)try...catch 捕獲異常

document.body.appendChild(document.createElement('p'))
function fun() {
  try {
    // Correct: document.querySelector('p').style.color = 'red'
    document.querySelector('.p').style.color = 'red'
  } catch (e) {
    console.log("Catch a error: ", e.message)
  } finally {
    console.log("Finally")
  }
}
fun()
  • 用於捕獲錯誤資訊
  • try 中寫入可能會發生錯誤的程式碼
  • catch 中寫入捕獲錯誤後的處理
  • finally 中寫入無論是否出錯都會執行的程式碼

(3)debugger

const btn = document.createElement('button')
btn.onclick = function () {
  debugger
}
btn.textContent = 'Debug'
document.body.appendChild(btn)
  • 使用 debugger 可以進入逐步除錯

0x03 處理 this

(1)this 指向

a. 普通函式

  • 普通函式的呼叫方式決定了 this 的值

    const obj = {
      a: function fun() {
        console.log(this);
      }
    }
    obj.a() // {a:f}
    
  • 沒有明確的呼叫方法時,this 的值為 window

    function fun() {
      console.log(this) // [object Window]
    }
    fun()
    
  • 嚴格模式下沒有明確的呼叫方法時,this 的值為 undefined

    'use strict'
    function fun() {
      console.log(this) // undefined
    }
    fun()
    

b. 箭頭函式

  • 箭頭函式不存在 this

    • 箭頭函式會預設繫結外層 this 的值
    • 箭頭函式中的 this 引用的是最近作用域中的 this
    • 箭頭函式會向外層作用域中一層層查詢 this,直至找到有 this 的定義
    const obj = {
      a: () => {
        console.log(this)
      }
    }
    obj.a()	// [object Window]
    
  • 在開發中,使用箭頭函式前需要考慮函式中 this 的值

    const btn = document.createElement("button")
    btn.textContent = "Click"
    btn.addEventListener("click", function() {
      console.log(this) // <button>Click</button>
    })
    btn.addEventListener("click", () => {
      console.log(this) // [object Window]
    })
    document.body.appendChild(btn)
    
  • 基於原型的物件導向不推薦採用箭頭函式

    function Obj() {}
    Obj.prototype.a = () => {
      console.log(this)
    }
    const obj = new Obj()
    obj.a()	// [object Window]
    

(2)改變 this

  • 有三個方法可以動態指定普通函式中 this 的指向

a. call()

  • 語法:call(thisArg, arg1, arg2, ...)

    • thisArg:在函式執行時,指定 this 的值
    • arg1, arg2, ...:傳參
    • 返回值就是函式的返回值
  • 舉例

    const obj = { a: 0 }
    function fun(x, y) {
      console.log(x, y, this) // 1 2 {a: 0}
    }
    fun.call(obj, 1, 2)
    

b. apply()

  • 語法:fun.apply(thisArg, [argsArray])

    • thisArg:在函式執行時,指定 this 的值
    • argsArray:傳參,必須包含在陣列裡面
    • 返回值就是函式的返回值
    • 因此 apply 主要跟陣列有關係
  • 舉例

    function sum(x, y) {
      console.log(this) // [object Window]
      return x + y
    }
    console.log(sum.apply(null, [1, 2])) // 3
    console.log(Math.max.apply(Math, [1, 2, 3])) // 3
    

c. bind()

  • bind 方法不會呼叫函式,但是也可以改變函式內部的 this 指向

  • 語法:bind(thisArg, arg1, arg2, ...)

    • thisArg:在函式執行時,指定 this 的值
    • arg1, arg2, ...:傳參,必須包含在陣列裡面
    • 返回值由指定的 this 值和初始化引數改造的原函式複製
  • 舉例

    const obj = { a: 0 }
    function fun(x, y) {
      console.log(x, y, this)
    }
    fun.bind(obj, 1, 2)() // 1 2 {a: 0}
    

0x04 效能最佳化

(1)防抖

  • 防抖(debounce):單位時間內,頻繁觸發事件,只執行最後一次

    • 觸發事件後,在 \(n\) 秒內函式只能執行一次,如果在 \(n\) 秒內再次被觸發,則重新計算函式執行時間
  • 使用場景:常用於輸入事件的處理中,以減少不必要的計算或操作

  • 舉例:滑鼠在盒子上移動,每 500ms 盒內數字加一

    const box = document.createElement('div')
    document.body.appendChild(box)
    
    let cnt = 1
    function add() {
      box.innerHTML = cnt++
    }
    
    function debounce(func, timeMs) {
      let timer
      return function () {
        if (timer) clearTimeout(timer)
        timer = setTimeout(function () {
          func()
        }, timeMs)
      }
    }
    
    box.addEventListener("mousemove", debounce(add, 500))
    
  • 防抖函式的封裝說明

    /**
     * @param {Function} func 要執行的函式。
     * @param {number} timeMs 延遲的時間,單位為毫秒。
     * @returns {Function} 返回一個新的函式,該函式具有防抖功能。
     */
    function debounce(func, timeMs) {
      let timer // 用於儲存定時器的變數
    
      // 返回一個新的函式,該函式會延遲執行傳入的func函式
      return function () {
        if (timer) clearTimeout(timer) // 如果存在定時器,則清除,以防止之前設定的執行被觸發
    
        // 設定一個新的定時器,當延遲時間過去後,執行func函式
        timer = setTimeout(function () {
          func()
        }, timeMs)
      }
    }
    

(2)節流

  • 節流(throttle):單位時間內,頻繁觸發事件,只執行一次

    • 連續觸發事件,但在 \(n\) 秒內僅執行一次函式
  • 使用場景:常用於高頻事件的處理中,以減少不必要的效能消耗

  • 舉例:滑鼠在盒子上移動,每 500ms 盒內數字加一

    const box = document.createElement('div')
    document.body.appendChild(box)
    
    let cnt = 1
    function add() {
      box.innerHTML = cnt++
    }
    
    function throttle(func, timeMs) {
      let timer = null
      return function () {
        if (!timer) {
          timer = setTimeout(function() {
            func()
            timer = null
          }, timeMs)
        }
      }
    }
    
    box.addEventListener("mousemove", throttle(add, 500))
    
  • 節流函式封裝說明

    /**
     * @param {Function} func 要節流的函式
     * @param {number} timeMs 節流的時間間隔(毫秒)
     * @returns {Function} 返回一個新函式,新函式將控制原函式在指定時間間隔內只執行一次
     */
    function throttle(func, timeMs) {
      let timer = null // 利用閉包儲存一個定時器變數
    
      return function () {
        // 如果定時器不存在,則設定定時器
        if (!timer) {
          timer = setTimeout(function() {
            func() // 在指定時間間隔後執行原函式
            timer = null // 執行後重置定時器變數
          }, timeMs)
        }
      }
    }
    

-End-

相關文章