通過自己實現函數 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刪除)。或許你已經明白了,但是程式碼種東西,還是自己動動手,印象更深刻。

