原生JS實現call、apply、bind(超詳細!)

想要實現實現一個方法,必然要先了解它。其實call、apply、bind都是咱們平常開發中比較經常使用的一些方法。他們都有一個共同的做用:改變this指向數組

call和apply本質上來講並無什麼區別,他們的做用是同樣,區別僅在於param的傳參方式。call接受連續傳參,apply接受數組傳參。markdown

bind和call\apply的區別想對來講就大一些,call\apply都是當即執行的,它們的返回結果就是fn的執行結果;而bind並不當即執行,它的返回結果是fn的拷貝,改變this指向後不會當即執行,須要自行調用這個新函數。app

/** @params: targetThis (可選) fn的this的目標指向,默認指向window @params: param (可選) 傳入fn的參數 */
fn.call(targetThis, param1, param2, param3 ...)
fn.apply(targetThis, [param1, param2, param3 ...])
fn.bind(targetThis, param1, param2, param3..)
(PS: 爲了統一,下面文章都用fn、targetThis、params來敘述
複製代碼
  • call的實現函數

    call的做用只說一句改變this指向就太籠統了,它的執行能夠大體拆分爲以下幾個步驟:ui

    1. 將傳入的fn做爲targetThis的私有方法,使fn能夠在this指向targetThis的前提下執行
    2. 經過arguments截取連續傳入的不定量的參數
    3. 執行fn、記錄執行結果
    4. 刪除臨時掛在targetThis的fn(不改變原來的targetThis)
    5. 返回執行結果
Function.prototype._call = function (context = window) {
  // 首先了解各個參數的值
  console.log('this', this) // foo
  console.log('context', context) // target {value: 1}
  console.log('arguments', arguments) // { {value: 1}, 'param1', 'param2'}
  let _context = context // 第一個參數不傳默認爲window
  _context.fn = this // 將foo做爲target的私有方法, this改變
  const args = [...arguments].slice(1) // 截取下標從1開始的參數 {'param1', 'param2'}
  const result = _context.fn(...args) // 當即執行
  delete _context.fn // 刪除fn 不改變target
  return result
}
// eg
let target = {
  value: 1
}
function foo(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
foo._call(target, 'param1', 'param2') // param1 param2 1
複製代碼
  • apply的實現

apply的實現與call基本上是一致的,區別僅在於params的形式上,apply的參數爲數組形式。this

Function.prototype._apply = function (context = window, arr) {
  // 首先了解各個參數的值
  console.log('this', this) // fn
  console.log('context', context) // target {value: 1}
  let _context = context// 第一個參數不傳默認爲window
  _context.fn = this // 將fn做爲target的私有方法, this改變
  const result = arr.length ? _context.fn(...arr) : _context.fn() // 判斷是否傳入arr
  delete _context.fn // 刪除fn 不改變target
  return result
}
// eg
let target = {
  value: 1
}
function fn(param1, param2) {
  console.log(param1)
  console.log(param2)
  console.log(this.value)
}
fn._apply(target, ['param1', 'param2']) // param1 param2 1
複製代碼
  • bind的實現

因爲ES5內置的Function.prototype.bind(..)實現過於複雜,這裏咱們藉助call/apply來實現bind,在實現bind以前咱們明確幾個重點:spa

  1. bind並非當即執行的,它會返回一個函數,調用返回的函數改變this指向
  2. 在this的綁定規則中, new綁定的優先級高於硬綁定(劃重點,待會要考)
  3. MDN中的原話:

綁定函數也可使用 new 運算符構造,它會表現爲目標函數已經被構建完畢了似的。提供的 this 值會被忽略,但前置參數仍會提供給模擬函數。prototype

(劃重點加感嘆號)code

咱們來解讀一下, 「綁定函數也可使用new運算構造 」 就是說會在new中使用硬綁定,而new綁定的優先級又高於硬綁定,咱們須要對有new的狀況作特殊處理;「前置參數仍會提供給模擬函數」就是說,在new中使用硬綁定函數能夠預先設置一些參數,在使用new進行初始化時能夠傳入其餘參數,是柯里化的一種。orm

Function.prototype._bind = function (context = window) {
    // 首先搞清楚各個參數的含義
    console.log('this', this) // foo
    console.log('context', context) // target {value: 1}
    console.log('argument', arguments) // { {value1: 1}, 'param1', 'param2'}
    const _this = this // 保存this
    // 這裏arguments爲foo的arguments
    const args = [...arguments].slice(1) // 截取下邊爲1開始的參數 ['param1', 'param2']
    var fn =  function () {
        const bindArgs = [...arguments] // 這裏的arguments爲fn的
        // 做爲構造函數時,this指向實例fn this instanceof fn 返回true
        // 不做爲構造函數時, this指向window, this instanceof fn 返回false 須要將this指向改變爲context
        // [...args, ...bindArgs] 合併預先傳入的參數和new 實例化時傳入的參數
        return _this.apply( this instanceof fn ? this : context , [...args, ...bindArgs ])
    }
    // 修改綁定函數的prototype爲foo的prototype,繼承foo的屬性
    // 建立一個空對象,讓空對象__proto__指向_this.prototype, 作到修改fn的prototye不會影響到foo
    fn.prototype = Object.create(_this.prototype)
    return fn
}
// eg
let target = {
    value: 1
}
function foo(param1, param2, param3) {
    console.log(param1)
    console.log(param2)
    console.log(param3)
    console.log(this.value)
}
const bindFn = foo._bind(target, 'param1', 'param2') 
bindFn() // param1 param2 undefined 1

const newBindFn = new bindFn('param3') // param1 param2 param3 undefined
複製代碼
相關文章
相關標籤/搜索