最近一直忙於實習以及畢業設計的事情,因此上週閱讀源碼以後本週就一直沒有進展。今天在寫完開題報告以後又抽空看了一眼Underscore源碼,發現上次沒有看明白的一個函數突然就豁然開朗了,因而趕忙寫下了這篇筆記。html
關於如何綁定函數this指向,一直是JavaScript中的高頻話題,面試時考官也喜歡問如何綁定函數this的指向,以及如何試現一個bind函數,今天咱們就從Underscore源碼來學習如何實現一個bind函數。git
在學習源碼以前,咱們最好先了解一下函數中this的指向,我在這個系列以前有寫過一篇文章,比較完善的總結了一下JavaScript函數中this的指向問題,詳情參見:博客園。github
另外,在學習_.bind
函數以前,咱們須要先了解一下Underscore中的重要工具函數——restArgs
。就在個人上一篇文章中就有介紹到:理解Underscore中的restArgs函數。面試
在學習_.bind
函數以前,咱們先來看一下Underscore中的另外一個工具函數——executeBound。由於這是一個重要的工具函數,涉及到bind的實現。瀏覽器
executeBound源碼(附註釋):閉包
// Determines whether to execute a function as a constructor
// or a normal function with the provided arguments.
//執行綁定函數,決定是否把一個函數做爲構造函數或者普通函數調用。
var executeBound = function (sourceFunc, boundFunc, context, callingContext, args) {
//若是callingContext不是boundFunc的一個實例,則把sourceFunc做爲普通函數調用。
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
//不然把sourceFunc做爲構造函數調用。
//baseCreate函數用於構造一個對象,繼承指定的原型。
//此處self就是繼承了sourceFunc.prototype原型的一個空白對象。
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
//這裏之因此要判斷一下是由於若是構造函數有返回值而且返回值是一個對象,那麼新構造的對象就會是返回值,而非this所指向的值。
if (_.isObject(result)) return result;
//只有在構造函數沒有返回值或者返回值時非對象時,才返回this所指向的值。
return self;
};
複製代碼
首先咱們先看爲何在executeBound函數結尾須要判斷一下result,緣由已經寫明在註釋裏,請你們必定仔細注意! 舉一個幫助理解的例子:app
var A = function() {
this.name = 'A';
return {};
}
var B = function() {
this.name = 'B';
}
var C = function() {
this.name = 'C';
return 'C';
}
var a = new A();
var b = new B();
var c = new C();
複製代碼
在瀏覽器中輸出a、b、c,看看你會發現什麼?而後再來仔細思考代碼中註釋的部分吧。ide
其次回到咱們這篇文章的重點,這個函數的功能很是好理解,就是根據實際狀況來決定是否把一個函數(sourceFunc)當作構造函數或者普通函數來調用。這個根據的條件就是看callingContext參數是不是boundFunc函數的一個實例。若是callingContext是boundFunc的一個實例,那麼就把sourceFunc當作一個構造函數來調用,不然就當作一個普通函數來調用,使用Function.prototype.apply來改變sourceFunc中this的指向。函數
單獨開這個函數可能會使咱們變得疑惑,爲何要這麼作呢?這個callingContext跟boundFunc是什麼關係?爲何要根據這兩個參數的關係來決定是否以構造函數的形式調用sourceFunc。工具
接下來咱們根據實際情景來解析這段源碼。
在Underscore源碼中,使用ctrl + F
鍵查找executeBound
字段,共有三處結果。其中一處是上方源碼所示的executeBound函數聲明。另外兩處是調用,其形式都以下所示:
var bound = restArgs(function (callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
複製代碼
能夠注意到實際調用時,第四個參數(callingContext)都是this,表明當前bound函數執行做用域,而第二個參數是bound自身,這樣的寫法着實奇怪。
其實考慮到咱們的目的也就不難理解爲何這麼寫了,由於當咱們把bound函數當作構造函數調用時,構造函數(此時也就是bound函數)內部的this會指向新構造的對象,而這個由bound函數新構造的對象天然就是bound函數的一個實例了,此時就會把sourceFunc當作構造函數調用。
接下來咱們再看_.bind
函數,一塊兒深刻理解該函數的同時,順便理解一下executeBound函數中爲何要根據callingContext和boundFunc的關係來肯定sourceFunc的調用方式。
咱們先看_.bind
函數的源碼(附註釋):
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available.
//將指定函數中的this綁定到指定上下文中,並傳遞一些參數做爲默認參數。
//其中args是默認參數,之後調用新的func時無需再次傳遞這些參數。
_.bind = restArgs(function (func, context, args) {
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
var bound = restArgs(function (callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
return bound;
});
複製代碼
咱們看到在_.bind
函數的內部定義了一個bound函數,而後返回了這個函數,即爲閉包。閉包的好處即在於內部的函數是私有函數,能夠訪問外部函數做用域,在內部函數調用以前,整個外部函數的做用域都是存在且對於內部函數而言是可訪問的。在restArgs函數的參數(即匿名函數)中並無處理如何調用func,由於咱們要根據狀況來決定。當咱們使用_.bind
函數綁定一個函數的this時,會返回bound函數做爲新的func函數,而bound函數會根據其調用的方式,來決定如何調用func,而此處的閉包可以保證在bound執行以前,func是一直存在的。當咱們使用new來操做bound函數構造新的對象時,bound內的this指向新構造的對象(即爲bound的新實例),executeBound函數內部就會把func當作構造函數來調用;若是以普通函數形式調用bound,那麼內部的this會指向外部調用bound函數時的做用域,天然就不是bound的一個實例了,這就是爲何會給executeBound第四個參數傳遞this的緣由。
口說無憑,咱們本身寫個代碼探究一下閉包內部函數中this的指向問題:
var test = function() {
var bound = function() {
this.name = 'bound';
console.log(this);
}
return bound;
}
var Bound = test();
var b = new Bound();
var b = Bound();
//bound { name: 'bound' }
//window
複製代碼
你們能夠將上面這段代碼拷貝到瀏覽器控制檯試一試,看看結果是否是跟上面的註釋同樣。
經過上面的學習,咱們知道了原來bind函數還要考慮到特殊狀況——被綁定過this的函數做爲構造函數調用時的狀況。 接下來咱們手動實現一個簡單的bind函數:
var _bind = function(func, context) {
var bound = function() {
if(this instanceof bound) {
var obj = new Object();
obj.prototype = func.prototype;
obj.prototype.constructor = func;
var res = func.call(obj);
if(typeof res == 'function' || typeof res == 'object' && !!res)
return res;
else
return obj
}
else {
return func.call(context);
}
};
return bound;
}
複製代碼
在閱讀這篇文章以前,你會如何實現一個bind函數呢?
更多Underscore源碼解讀:GitHub