一文搞懂JavaScript中bind,apply,call的實現
bind、call和apply都是Function
原型鏈上面的方法,因此不管是使用function
聲明的函數,還是箭頭函數都可以直接調用。這三個函數在使用時都可以改變this
指向,本文就帶你看看如何實現bind、call和apply。
bind、call和apply的用法
bind
bind()
方法可以被函數對象調用,并返回一個新創(chuàng)建的函數。
語法:
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind()
會將第一個參數作為新函數的this
,如果未傳入參數列表,或者第一個參數是null
或undefined
,那么新函數的this
將會是該函數執(zhí)行作用域的this
。使用bind()
應注意以下事項:
- 返回一個新的函數,但是不會立即執(zhí)行該函數
- 根據傳入的參數列表綁定
this
指向,如果未傳入thisArg
,那么需要明確this的指向 - 如果是箭頭函數,無法改變this,只能改變參數,這一點我們在這些情況下不建議你使用箭頭函數也講到過
舉個例子:
正常使用
function fn(a) { console.log(this, a) } const fn1 = fn.bind({x: 100}); // fn1是一個函數,但是并沒有立即執(zhí)行 fn1(); // {x:100} 100 console.log(fn === fn1); // false,bind返回的是一個新的函數
箭頭函數
const fn = (a) => { console.log(this, a); } const fn1 = fn.bind({x: 100}, 100); // 返回一個新的函數fn1,不會執(zhí)行 fn1(); // window,100 箭頭函數通過bind返回的函數無法修改其this指向
未綁定this,或綁定到null、undefined
const fn = (a) => { console.log(this, a); } const fn1 = fn.bind(); // 未綁定 const fn2 = fn.bind(null); // 綁定null const fn3 = fn.bind(undefined); // 綁定undefined fn1(); // 綁定到執(zhí)行作用域,默認為window fn2(); // 綁定到執(zhí)行作用域,默認為window fn3(); // 綁定到執(zhí)行作用域,默認為window
call&apply
與bind
不同,call
和apply
都是用來執(zhí)行函數的,可以解決執(zhí)行的函數的this指向問題。
語法:
function.call(thisArg, arg1, arg2, ...) function.apply(thisArg, argsArray)
call
的參數列表是可選的,如果傳入的thisArg
是null
或者undefined
,那么會自動替換為全局對象;如果是傳入的原始值,則會替換為原始值對應的包裝類型。apply
的用法和call
類似,不同點在于其額外傳入的參數是一個數組或類數組對象,而call
的額外參數是不確定參數。
舉個栗子:
function fn(a, b) { console.log(this, a, b); } fn.call({x: 100}, 10, 20); // {x: 100} 10 20 fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call
和apply
無法修改箭頭函數的this指向:
const fn = (a, b) => { console.log(this, a, b); } fn.call({x: 100}, 10, 20); // Window 10 20 fn.apply({x: 100}, [10, 20]); // Window 10 20
簡單回顧了以下bind、call、apply的使用,接下來就看看應該如何來實現。
實現bind
根據我們剛剛使用的bind()
,在設計時需要如下考慮:
- 最終返回的是一個新的函數,可通過
function
來聲明 - 需要綁定新函數的this
- 需要綁定運行時的參數,可通過apply或call來實現
實現代碼:
// 通過原型鏈注冊方法 // context:傳遞的上下文this;bindArgs表示需要綁定的額外參數 Function.prototype.newBind = function (context, ...bindArgs) { const self = this; // 當前調用bind的函數對象 // 返回的函數本身也是可以再傳入參數的 return function (...args) { // 拼接參數 const newArgs = bindArgs.concat(args); return self.apply(context, newArgs) } } function fn(a,b) { console.log(this, a, b); } const fn1 = fn.newBind({x: 100}, 10); fn1(20); // {x: 100} 10 20
bind()
返回的是一個新函數,執(zhí)行新函數就相當于是通過call
或apply
來調用原函數,并傳入this和參數。
實現call和apply
在實現bind
的過程中,我們使用了apply
來完成this的綁定,那么要實現apply
又應該用什么來綁定this呢?可能會有小機靈鬼發(fā)現,好像在apply
中使用call
,在call
中使用apply
也可以完成this綁定。這不就形成了嵌套嘛,不是我們最終想要的。
我們先來
call和apply的應用:
- bind返回一個新的函數,并不會執(zhí)行;call和apply會立即執(zhí)行函數
- 綁定this
- 傳入執(zhí)行參數
舉個栗子:
function fn(a, b) { console.log(this, a, b); } fn.call({x: 100}, 10, 20); // {x: 100} 10 20 fn.apply({x: 100}, [10, 20]); // {x: 100} 10 20
call和apply的實現效果是一樣的,都是立即執(zhí)行函數,不同的是call需要傳入單個或者多個參數,apply可以傳入一個參數數組。
如何在函數執(zhí)行時綁定this:
- const obj = {x: 100, fn() {this.x}}
- 執(zhí)行obj.fn(),此時fn()內部的this指向的就是obj
- 可以借此實現函數綁定this
使用過Vue的朋友都知道,Vue實例其實就是一個對象,其里面的方法在調用時,this就會指向當前對象。舉個栗子:
let obj = { key: 'key', getKey: () => { return this.key; }, getKey2() { return this.key; } }; obj.getKey(); // this指向window,返回值取決于window中是否有對應的屬性 obj.getKey2(); // this指向obj,返回 'key'
這個例子在這些情況下不建議你使用箭頭函數也是有提及的,感興趣的朋友可以去看看。根據此原理,我們就可以來嘗試給函數綁定this了:某函數調用apply
,那么我們就將這個函數添加到傳入的this對象中(如果未傳入則this為全局對象,如果傳入的是原始值,則使用其包裝類型),然后使用()
來執(zhí)行函數,這個時候函數的this指向的就是我們傳入的this了。
實現代碼:
Function.prototype.newCall = function(context, ...args) { if (context == null) context = globalThis; // 如果傳入的上下文是null或者undefined,則使用全局globalThis,一般指向的就是window if (typeof context !== 'object') context = new Object(context); // 如果是原始類型(數字、字符串、布爾值等),則使用其包裝類型 const fnKey = Symbol(); // 使用Symbol可確保key值不會重復,避免屬性覆蓋 context[fnKey] = this; // this指向的是當前調用newCall的函數 console.log(context[fnKey]); // 打印當前函數以及上下文this console.log(context); const res = context[fnKey](...args); // 執(zhí)行函數,函數的this指向為context delete context[fnKey]; // 刪除fn,防止污染 return res; // 返回結果 } fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20 function fn(a,b) { console.log(this, a, b); }
這樣我們就實現了call
,那么apply
實現類似,只不過傳入的額外參數要變成數組或類數組的方式
Function.prototype.newCall = function(context, args) { if (context == null) context = globalThis; // 如果傳入的上下文是null或者undefined,則使用全局globalThis,一般指向的就是window if (typeof context !== 'object') context = new Object(context); // 如果是原始類型(數字、字符串、布爾值等),則使用其包裝類型 const fnKey = Symbol(); // 使用Symbol可確保key值不會重復,避免屬性覆蓋 context[fnKey] = this; // this指向的是當前調用newCall的函數 console.log(context[fnKey]); // 打印當前函數以及上下文this console.log(context); const res = context[fnKey](...args); // 執(zhí)行函數,函數的this指向為context delete context[fnKey]; // 刪除fn,防止污染 return res; // 返回結果 } fn.newCall({x: 100}, 10, 20); // {x: 100} 10 20 function fn(a,b) { console.log(this, a, b); }
注意打印的當前函數以及上下文:
實現call
和apply
與bind
有很大的不同就是如何來處理this
綁定。
總結
學會了如何實現bind、call和apply,對于理解如何使用,以及如何避免潛在的錯誤有很大的幫助。特別是call
和apply
,我們在實現的時候借助于對象內部的非箭頭函數,其this指向對象自身這一基礎知識,實現了this綁定。如果還未搞清楚的朋友,可以將代碼運行起來看看,也許能幫助你更好的理解。
以上就是一文搞懂JavaScript中bind,apply,call的實現的詳細內容,更多關于JavaScript bind apply call的資料請關注腳本之家其它相關文章!
相關文章
深入理解JavaScript系列(26):設計模式之構造函數模式詳解
這篇文章主要介紹了深入理解JavaScript系列(26):設計模式之構造函數模式詳解,本文講解了基本用法、構造函數與原型、只能用new嗎?、強制使用new、原始包裝函數等內容,需要的朋友可以參考下2015-03-03跟我學習javascript的for循環(huán)和for...in循環(huán)
跟我學習javascript的for循環(huán)和for...in循環(huán),它們是JavaScript中提供了兩種方式迭代對象,本文就和大家一起學習for循環(huán)和for...in循環(huán),感興趣的小伙伴們可以參考一下2015-11-11Javascript中類式繼承和原型式繼承的實現方法和區(qū)別之處
其它的面向對象程序設計語言都是通過關鍵字來解決繼承的問題。但是javascript中并沒有定義這種實現的機制。接下來通過本文給大家介紹Javascript中類式繼承和原型式繼承的實現方法和區(qū)別,需要的朋友可以參考下2017-04-04javascript中的nextSibling使用陷(da)阱(keng)
關于HTML/XML節(jié)點的問題,在IE中nextSibling不會返回文本節(jié)點,而chrome或者firefox等會返回文本節(jié)點2014-05-05