通過自己實現函數 call,apply,bind 來了解他們的原理

  • 2019 年 11 月 28 日
  • 筆記

我們知道函數中的call,apply,bind都是可以修改函數的this指向。關於函數的this指向問題可以轉到Javascript this 指向問題這篇文章。

在Javascript this 指向問題一篇中我們知道函數運行時候this是取決於調用這個函數的對象。如果一個函數定義在了全局,那麼這個this就指向window。

來看一段程式碼

var name = "tal"  var sex = "boy"    var tal = {    name: "踏浪",    sex: "男"  }    function person() {    console.log(this.name)    console.log(this.sex)  }  person() // tal boy

上面的person是定義在全局中的一個函數,person()調用的時候相當於是window.person(),是由window調用的,所以,this.name和this.sex都需要在window對象中尋找,即全局變數中找。所以,最後的值會是tal與boy。

再來。我們來調用一下原生的call函數

var name = "tal"  var sex = "boy"    var tal = {    name: "踏浪",    sex: "男"  }    function person() {    console.log(this.name)    console.log(this.sex)  }  person.call(tal) // 踏浪 男

在調用了call函數以後,最後的值是踏浪 男。得到的是對象tal中的兩個屬性值。此時函數中的this指向了對象tal。結合上面的例子,直接調用person函數的時候,相當於是window.person()的執行,所以那時this指向了window。那現在既然指向了tal,換言之,就是相當於tal.person()執行了。

call 的實現

所以我們可以這樣實現mycall函數

Function.prototype.mycall = function (context) {    context.fn = this    context.fn()    delete context.fn  }

mycall接收一個參數,即一個對象,最終的this指向這個對象。函數內部實現在這個傳入的對象中綁定上我們需要執行的這個函數,即context.fn = this一行。最後調用context.fn()。因為我們這樣操作修改了傳入對象的屬性(添加了一個fn屬性),所以最後需要刪除這個fn屬性。這樣,第一版的call的實現已經完成。

接下來,原生 call 函數是可以接收參數的

var tal = {    name: "踏浪",    sex: "男"  }    function person(age) {    console.log(age)    console.log(this.name)    console.log(this.sex)  }  person.call(tal, 18) // 踏浪 男

那我們自己實現的mycall要想能夠接收參數,怎麼實現呢

Function.prototype.mycall = function (context) {    var args = [];    var len = arguments.length;    for (var i = 1; i < len; i++) {      args.push(arguments[i]);    }    context.fn = this    eval('context.fn(' + args.join(",") + ')')    delete context.fn  }

因為我們需要在 mycall 調用的時候傳遞參數,而且參數的個數不確定,所以需要使用 arguments 。同時因為第一個參數以及確定了是我們需要的一個對象,this指向這個對象。所以 arguments 需要從 1 開始。我們用一個數組把需要的東西存放起來。

var args = [];  var len = arguments.length;  for (var i = 1; i < len; i++) {    args.push(arguments[i]);  }

如果是這樣的話,我傳遞過去後數組 args = [18, "成都"] 裡面的每一項要怎麼傳遞到 context.fn() 裡面並且執行呢?或許你會想到時候 join(",") 方法,OK,沒有問題。

var res = args.join(",");  context.fn(res);

這樣么?不是。res的值是 18,成都 沒問題,但是這就是一個字元串而已,我們要實現傳遞兩個參數,所以,想到了把字元串與函數拼接。即有了 'context.fn(' + args.join() + ')'。這樣最後就可以了。但是這只是一個字元串,怎麼運行呢?這個時候就需要用到 ES3中的eval 直接傳遞這個參數進度即可。

上面的 join 方法其實就是把一個數組轉換成了字元串,除了 join,你還能想到什麼呢?對。可以使用字元轉換來實現。

res = "" + args + ""  // or  res = args.toString()

因為每一個隔開的方式是 , 所以可以使用上面的兩種,其他的就不行咯。

所以最終的 mycall

Function.prototype.mycall = function (context) {    var args = [];    var len = arguments.length;    for (var i = 1; i < len; i++) {      args.push(arguments[i]);    }    context.fn = this    eval('context.fn(' + args.join(",") + ')')    delete context.fn  }

apply的實現

有個實現call的過程,在來實現apply就容易多了。唯一不同的就是apply傳遞的參數是一個數組,而call是具體的每一項。只需要在參數上面做處理即可。

Function.prototype.myapply = function (context) {    var args = [];    var array = arguments[1];    if ( typeof array !== "object" || !(array instanceof Array) ) {      throw new Error("The 2'rd args must be Array.")    }    for (var i = 0; len = array.length, i < len; i++) {      args.push('array[' + i + ']');    }    context.fn = this    eval('context.fn(' + args.toString() + ')')    delete context.fn  }

這裡的 arguments[1] 是一個數組了。我們需要對它遍歷,並且判斷它是不是一個數組。其餘的與 call 一樣。

bind的實現

原生的bind有兩種方式

var tal = {    name: "踏浪",    sex: "男"  }    function person(age) {    console.log(age)    console.log(this.name)    console.log(this.sex)  }    person.bind(tal)(18)    // 或者  person.bind(tal, 18)()

所以。使用bind都需要調用兩次,而第一次就是返回一個函數。原函數的參數可以在bind中調用,也可以在第二次運行時候調用。所以,根據調用bind時候傳遞的參數的個數確定最後是返回那種函數,有了下面的這段程式碼。

Function.prototype.mybind = function (context) {    var args = [];    var len = arguments.length;    for (var i = 1; i < len; i++) {      args.push(arguments[i]);    }    context.fn = this    if (len === 1) {      // 如果 mybind 的參數的個數只有一個,那麼剩餘參數在調用的時候傳入      return function(){        var sub_args = [];        for (var i = 0; len = arguments.length, i < len; i++) {          sub_args.push('arguments[' + i + ']');        }        eval('context.fn(' + sub_args.toString() + ')')        delete context.fn      }    } else {      return function(){        // 這裡不能使用 arguments,這裡面的 arguments 是這個 return 函數的而不是最開始的。        var str = 'context.fn('        for (var i = 0; len = args.length, i < len; i++) {          str += "args["+i+"],"        }        var newStr = str.replace(/,$/, ")")        eval(newStr)        delete context.fn      }    }  }

總結

通過自己實現 call, apply, bind 這三種方法,能夠更深刻的理解到這三個函數的原理,同時涉及到的只是點也多:this指向,arguments類數組,每一個對象都要的toSting方法(另一個是valueOf),eval方法的使用(不是濫用,webpack中就使用了這個方法),函數對象可以使用 delete 刪除(使用var 定義的無法使用delete刪除)。或許你已經明白了,但是程式碼種東西,還是自己動動手,印象更深刻。

Tags: