理解Underscore中的_.bind函式

Russ_Zhong發表於2019-01-25

最近一直忙於實習以及畢業設計的事情,所以上週閱讀原始碼之後本週就一直沒有進展。今天在寫完開題報告之後又抽空看了一眼Underscore原始碼,發現上次沒有看明白的一個函式忽然就豁然開朗了,於是趕緊寫下了這篇筆記。

關於如何繫結函式this指向,一直是JavaScript中的高頻話題,面試時考官也喜歡問如何繫結函式this的指向,以及如何試現一個bind函式,今天我們就從Underscore原始碼來學習如何實現一個bind函式。

預備知識

在學習原始碼之前,我們最好先了解一下函式中this的指向,我在這個系列之前有寫過一篇文章,比較完善的總結了一下JavaScript函式中this的指向問題,詳情參見:部落格園

另外,在學習_.bind函式之前,我們需要先了解一下Underscore中的重要工具函式——restArgs。就在我的上一篇文章中就有介紹到:理解Underscore中的restArgs函式

工具函式——executeBound

在學習_.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,原因已經寫明在註釋裡,請大家一定仔細注意!
舉一個幫助理解的例子:

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,看看你會發現什麼?然後再來仔細思考程式碼中註釋的部分吧。

其次回到我們這篇文章的重點,這個函式的功能非常好理解,就是根據實際情況來決定是否把一個函式(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函式

我們先看_.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函式

通過上面的學習,我們知道了原來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

相關文章