前端面試之手寫一個bind方法
bind 函式對於寫react的人來說並不陌生。哦!是的,沒錯我的朋友,它的一個用處就是用來改變函式this指向的。如果細究一下bind的實現,發現裡面還是有不少東西的,我們今天展開討論一下。
在說bind之前呢,我們還要先來講講我們的老熟人 this。 說到this,我們在 ofollow,noindex" target="_blank">《前端面試之js相關問題(一)》 也有提到過,this的工作方式。今天我們再來看看它的四種繫結規則
This的四種繫結規則
1.預設繫結
獨立函式呼叫時, this
指向全域性物件,如果使用嚴格模式,那麼全域性物件無法使用預設繫結, this
繫結至 undefined
並拋錯(TypeError: this is undefined)
2.隱式繫結
當函式作為引用屬性被新增到物件中,隱式繫結規則會把函式呼叫中的 this
繫結到這個上下文物件
3.顯示繫結
運用apply call 方法,在呼叫函式時候繫結this,也就是指定呼叫的函式的this值
4.new繫結
就是使用new操作符的時候的this繫結
上述四條規則優先順序由上到下依次遞增。
由於js多樣的繫結規則,帶來了 繫結隱式丟失問題, 即函式中的 this
丟失繫結物件,即它會應用第 1 條的 預設繫結 規則,從而將 this
繫結到全域性物件或者 undefined
上。
例如:繫結至上下文物件的函式被賦值給一個新的函式,然後呼叫這個新的函式時
var obj = { a: 2, foo: function () { console.log(this.a) } } var a = 2 setTimeout(obj.foo, 0) // 2
還記得我們當年我們是怎麼做的嗎?
... var me = this; return function () { me.xxx() } ...
還有就是用call 或者apply來顯示的繫結:
function foo() { console.log( this.a); } var obj = { a: 2 }; var bar = function() { foo.call(obj); }; bar();// 2 setTimeout(bar, 100);// 2
由於這種用法太多了,所以呢ES5的時候給出了一個方法 JavaScript/Reference/Global_Objects/Function/bind" target="_blank" rel="nofollow,noindex">Function.prototype.bind() ,自從有了它,前端工程師的生活似乎好過了很多,一個bind可以解決很多問題。
先看bind定義
MDN 給出的定義是:
bind() 方法建立一個新的函式, 當這個新函式被呼叫時其this置為提供的值,其引數列表前幾項置為建立時指定的引數序列。
fun.bind(thisArg[, arg1[, arg2[, ...]]])
bind() 函式會建立一個新 繫結函式 , 繫結函式 與被調函式具有相同的函式體(在 ECMAScript 5 中)。呼叫 繫結函式 通常會導致執行 包裝函式 繫結函式 也可以使用new運算子構造:這樣做就好像已經構造了目標函式一樣。提供的 this 值將被忽略,而前置引數將提供給模擬函式
總的來說bind有如下三個功能點:
- 改變原函式的 this 指向,即繫結上下文,返回原函式的拷貝
- 當 繫結函式 被呼叫時,bind的額外引數將置於實參之前傳遞給被繫結的方法。
- 注意,一個 繫結函式 也能使用 new 操作符建立物件,這種行為就像把原函式當成構造器,thisArg 引數無效。也就是 new 操作符修改 this 指向的優先順序更高。
說了那麼多,我們現在可不可以實現一個bind呢?
其實有時候去研究這些JS API的實現還是蠻好玩的,你能學到很多知識。今天我們就手摸手寫一下吧。
從bind的定義描述中可以看到,我們要寫的這個函式的輸入輸出基本確定了:
- 輸入:接受一個或者多個引數,第一個是要繫結的上下文,額外引數當作繫結函式的前置引數。
- 輸出:返回原函式的拷貝,即返回一個函式,這個函式呢具備原函式的功能
// 定義這個方法為myBind Function.prototype.myBind = function(thisArg) { if (typeof this !== 'function') { return; } var _self = this; var args = Array.prototype.slice.call(arguments, 1) //從第二個引數擷取 return function() { return _self.apply(thisArg, args.concat(Array.prototype.slice.call(arguments))); // 注意引數的處理 } }
我們來測試一下:
function foo(name) { this.name = name; } var obj = {} //上下文 功能done var bar = foo.myBind(obj) bar('jack') console.log(obj.name) //'jack' // 引數 功能done var tar = foo.myBind(obj, 'rose'); tar() console.log(obj.name) //'rose' // new 功能error var alice = new bar('alice') console.log(obj.name) //aliceobj name should be 'jack' console.log(alice.name) //undefined, alice name should be 'alice'
可以看到使用 new
例項化被繫結的方法,上下文還指向了傳入的obj,這個方法有點問題,我們需要考慮到的是在myBind的實現裡面,需要檢測new的操作
我們先考慮一下new操作符在呼叫建構函式時做了哪些操作?
比如說 var a = new b()
{} .__proto__ = b.prototype
所以我們做了如下修改:
Function.prototype.myBind = function(thisArg) { if (typeof this !== 'function') { return; } var _self = this; var args = Array.prototype.slice.call(arguments, 1) var fnBound = function () { // 檢測 New // 如果當前函式的this指向的是建構函式中的this 則判定為new 操作 var _this = this instanceof _self ? this : thisArg; return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments))); } // 為了完成 new操作 // 還需要做一件事情 執行原型 連結 (思考題,為什麼? fnBound.prototype = this.prototype; return fnBound; }
測試OK了:
function foo(name) { this.name = name; } var obj = {}; var bar = foo.myBind(obj); bar('Jack'); console.log(obj.name);// Jack var alice = new bar('Alice'); console.log(obj.name);// Jack console.log(alice.name);// Alice
這個用例中我們來討論一下
bar的this指向 :
- 變數 bar 是繫結之後的函式,也就是 fnBound , _self 是 原函式 foo 的引用
- 如果直接 bar('jack') 它指向window 或undefined。
- 而 new bar('alice') (相當於 new foo('alice') )過程中, fnBound 的this指向了new表示式返回的物件alice
- 如果是 new 呼叫繫結函式,此時繫結函式中的 this 是由 new 呼叫繫結函式返回的例項物件,這個物件的建構函式是 fnBound ,
- 當我們忽略掉原型連線那行程式碼時,其原型物件並不等於原函式 self 的原型,所以 this instanceof self ? this : oThis 得到的值還是指定的傳入的物件,而不是 new 返回的物件
所以fnBound.prototype = this.prototype 是有必要的,!!但注意 這個原型賦值是有問題的:
原因我在這裡先不說了留給各位討論啦。
Function.prototype.myBind = function(thisArg) { if (typeof this !== 'function') { return } var _self = this var args = Array.prototype.slice.call(arguments, 1) var fnNop = function () {} // 定義一個空函式 var fnBound = function () { var _this = this instanceof _self ? this : thisArg return _self.apply(_this, args.concat(Array.prototype.slice.call(arguments))) } // 維護原型關係 if (this.prototype) { fnNop.prototype = this.prototype; } fnBound.prototype = new fnNop(); return fnBound; }
這裡我們建立了一個空函式來做中間人,承接原函式的原型丟給繫結函式,好了問題就搞完了,是不是很多知識點慢慢看吧,我去踢球去了。
參考:
JavaScript 中 this 的四條繫結規則
【誠聘英才】:joy:
我司最近要組建一直20多人的前端團隊,歡迎各路大神小白投簡歷喲:
我的郵箱:postbox:zhaofei#huohua . cn
我們公司,搜火花思維www.huohua.cn