前端面試之手寫一個bind方法
bind 函式對於寫react的人來說並不陌生。哦!是的,沒錯我的朋友,它的一個用處就是用來改變函式this指向的。如果細究一下bind的實現,發現裡面還是有不少東西的,我們今天展開討論一下。
在說bind之前呢,我們還要先來講講我們的老熟人 **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的時候給出了一個方法 Function.prototype.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) //alice obj 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;
}
這裡我們建立了一個空函式來做中間人,承接原函式的原型丟給繫結函式,好了問題就搞完了,是不是很多知識點慢慢看吧。
如果你覺得不錯,或者發現文章中的錯誤,或者有更好的建議,歡迎評論或進前端全棧群:866109386,來交流討論吹水。