使用JS簡單實現一下apply、call和bind方法

MomentYY發表於2022-02-19

使用JS簡單實現一下apply、call和bind方法

1.方法介紹

apply、call和bind都是系統提供給我們的內建方法,每個函式都可以使用這三種方法,是因為apply、call和bind都實現在了Function的原型上(Function.prototype),而他們的作用都是給我們函式呼叫時顯式繫結上this。下面先介紹一下它們的基本用法:

  • apply方法:呼叫一個具有給定this值的函式,以及以一個陣列(或類陣列物件)的形式提供的引數。

    • 使用語法:func.apply(thisArg, [argsArray])

      • thisArg:在func函式呼叫時繫結的this值;
      • [argsArray]:一個陣列或者類陣列物件,其中的陣列元素將作為單獨的引數傳給func函式;
    • 使用效果:

      function foo(x, y ,z) {
        console.log(this, x, y, z)
      }
      
      const obj = { name: 'curry', age: 30 }
      /**
       * 1.將obj物件繫結給foo函式的this
       * 2.陣列中的1 2 3分別傳遞給foo函式對應的三個引數
       */
      foo.apply(obj, [1, 2, 3])
      

  • call方法:使用一個指定的 this值和單獨給出的一個或多個引數來呼叫一個函式。

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

      • thisArg:在func函式呼叫時繫結的this值;
      • arg1, arg2, ...:指定的引數列表,將作為引數傳遞給func函式;
    • 使用效果:

      function foo(x, y ,z) {
        console.log(this, x, y, z)
      }
      
      const obj = { name: 'curry', age: 30 }
      /**
       * 1.將obj物件繫結給foo函式的this
       * 2.call剩餘引數中的a b c分別傳遞給foo函式對應的三個引數
       */
      foo.call(obj, 'a', 'b', 'c')
      

  • bind方法建立一個新的函式,在bind()被呼叫時,這個新函式的this被指定為bind()的第一個引數,而其餘引數將作為新函式的引數,供呼叫時使用。

    • 使用語法:func.bind(thisArg[, arg1[, arg2[, ...]]])

      • thisArg:呼叫func函式時作為this引數傳遞給目標函式的值;
      • arg1, arg2, ...:當目標函式被呼叫時,被預置入func函式的引數列表中的引數;
    • 使用效果:

      function foo(...args) {
        console.log(this, ...args)
      }
      
      const obj = { name: 'curry', age: 30 }
      /**
       * 1.將obj物件繫結給foo函式的this
       * 2.bind剩餘引數中的1 2 3分別傳遞給foo函式中引數
       * 3.也可在newFoo呼叫時傳入引數,這時bind傳遞的引數會與newFoo呼叫時傳遞的引數進行合併
       */
      const newFoo = foo.bind(obj, 1, 2, 3)
      newFoo()
      newFoo('a', 'b', 'c')
      

總結:

  • apply和call主要用於在函式呼叫時給函式的this繫結對應的值,兩者作用類似,主要區別就是除了第一個引數,apply方法接受的是一個引數陣列,而call方法接受的是引數列表。
  • bind也是給函式指定this所繫結的值,不同於apply和call的是,它會返回一個新的函式,新函式中的this指向就是我們所指定的值,且分別傳入的引數會進行合併。

2.apply、call和bind方法的實現

為了所有定義的函式能夠使用我們自定義的apply、call和bind方法,所以需要將自己實現的方法掛在Function的原型上,這樣所有的函式就可以通過原型鏈找到自定義的這三個方法了。

2.1.apply的實現

Function.prototype.myApply = function(thisArg, argArray) {
  // 1.獲取當前需要被執行的函式
  // 因為myApply是需要被當前函式進行呼叫的,根據this的隱式繫結,此處的this就是指向當前需要被執行的函式
  const fn = this

  // 2.對傳入的thisArg進行邊界判斷
  if (thisArg === null || thisArg === undefined) {
    // 當傳入的是null或者undefined是,被執行函式的this直接指向全域性window
    thisArg = window
  } else {
    // 將傳入的thisArg物件化,方便後面在thisArg新增屬性
    thisArg = Object(thisArg)
  }
  // 也可簡單寫成三元運算子:
  // thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

  // 3.將獲取的fn新增到thisArg物件上
  // 這裡使用Symbol的原因是避免外部傳入的thisArg中的屬性與新增fn有衝突
  const fnSymbol = Symbol()
  Object.defineProperty(thisArg, fnSymbol, {
    enumerable: false,
    configurable: true,
    writable: false,
    value: fn
  })
  // 也可簡單寫成
  // thisArg[fnSymbol] = fn

  // 4.對argArray進行判斷
  // 看是否有傳入值,沒有值傳入就預設 []
  argArray = argArray || []

  // 5.呼叫獲取的fn函式,並將對應傳入的陣列展開傳遞過去
  const result = thisArg[fnSymbol](...argArray)
  // 呼叫完後刪除新增的屬性
  delete thisArg[fnSymbol]

  // 6.將結果返回
  return result
}

測試:雖然列印出來的物件中還存在Symbol屬性,實際上已經通過delete刪除了,這裡是物件引用的問題。

function foo(x, y, z) {
  console.log(this, x, y, z)
}

foo.myApply({name: 'curry'}, [1, 2, 3])

2.2.call的實現

call方法的實現和apply方法的實現差不多,主要在於後面引數的處理。

Function.prototype.myCall = function(thisArg, ...args) {
  // 1.獲取當前需要被執行的函式
  const fn = this

  // 2.對傳入的thisArg進行邊界判斷
  thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

  // 3.將獲取的fn新增到thisArg物件上
  const fnSymbol = Symbol()
  thisArg[fnSymbol] = fn

  // 4.呼叫獲取的fn函式,並將對應傳入的args傳遞過去
  const result = thisArg[fnSymbol](...args)
  // 呼叫完後刪除新增的屬性
  delete thisArg[fnSymbol]

  // 5.將結果返回
  return result
}

測試:

function foo(x, y, z) {
  console.log(this, x, y, z)
}

foo.myCall({name: 'curry'}, 1, 2, 3)

2.3.bind的實現

bind方法的實現稍微複雜一點,需要考慮到引數合併的問題。

Function.prototype.myBind = function(thisArg, ...argsArray) {
  // 1.獲取當前的目標函式,也就是當前使用myBind方法的函式
  const fn = this

  // 2.對傳入的thisArg進行邊界判斷
  thisArg = (thisArg === null || thisArg === undefined) ? window : Object(thisArg)

  // 3.將獲取的fn新增到thisArg物件上
  const fnSymbol = Symbol()
  thisArg[fnSymbol] = fn

  // 4.定義一個新的函式
  function newFn(...args) {
    // 4.1.合併myBind和newFn傳入的引數
    const allArgs = [...argsArray, ...args]
    // 4.2.呼叫真正需要被呼叫的函式,並將合併後的引數傳遞過去
    const result = thisArg[fnSymbol](...allArgs)
    // 4.3.呼叫完後刪除新增的屬性
    delete thisArg[fnSymbol]

    // 4.4.將結果返回
    return result
  }

  // 6.將新函式返回
  return newFn
}

測試:

function foo(x, y, z) {
  console.log(this, x, y, z)
}

const newFoo = foo.myBind({ name: 'curry' }, 1, 2)
newFoo(3)

相關文章