最近在看《你不知道的JavaScript》系列,看到這個地方的時候,第一眼沒對上,沒有確認過的眼神,所以就帶著疑惑,深入解析一下,做了一份學習總結。
Function.prototype.bind
引用 MDN:
bind()
方法建立一個新的函式, 當被呼叫時,將其this
關鍵字設定為提供的值,在呼叫新函式時,在任何提供之前提供一個給定的引數序列。語法:
fun.bind(thisArg[, arg1[, arg2[, ...]]])複製程式碼
引數:
thisArg
:當繫結函式被呼叫時,該引數會作為原函式執行時的 this 指向。當使用 new 呼叫繫結函式時,該引數無效。
arg1, arg2, ...
:當繫結函式被呼叫時,這些引數將置於實參之前傳遞給被繫結的方法。返回值:
返回由指定的
this
值和初始化引數改造的原函式拷貝
從上面的定義來看,bind
函式有哪些功能:
改變原函式的
this
指向,即繫結this
返回原函式的拷貝
注意,還有一點,當
new
呼叫繫結函式的時候,thisArg
引數無效。也就是new
操作符修改this
指向的優先順序更高
bind 函式的實現
bind
函式的實現,需要了解 this
的繫結。this
繫結有 4 種繫結規則:
預設繫結
隱式繫結
顯式繫結
new 繫結
四種繫結規則的優先順序從上到下,依次遞增,預設繫結優先順序最低,new
繫結最高。今天我們來討論一下顯式繫結。
顯式繫結就是,運用 apply(...)
和 call(...)
方法,在呼叫函式時,繫結 this
,也即是可以指定呼叫函式中的 this
值。例如:
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
foo.call(obj); // 2複製程式碼
這是不是 bind
函式的功能之一,修改 this
的繫結?如果我們將上面的例子修改一下:
Function.prototype.myBind = function(oThis) {
if(typeof this !== 'function') {
return;
}
var self = this,
args = Array.prototype.slice.call(arguments, 1);
return function() {
return self.apply(oThis, args.concat(Array.prototype.slice.call(arguments)));
}
}
function foo() {
console.log(this.a);
}
var obj = { a: 2 };
var bar = foo.myBind(obj);
bar(); // 2複製程式碼
這便是一個簡易版的 bind
函式了,已實現了原生 bind
函式的前兩個功能點了。
但是,如果遇到 new
呼叫繫結函式(注意這裡哈,是繫結之後的函式)的時候,結果會是怎樣呢?
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); // Alice
console.log(alice.name); // undefined複製程式碼
我們發現,new
呼叫繫結函式,並不會更改 this
的指向,我們簡易版能做的,只是永久繫結指定的 this
。
如何實現原生 bind
的第三個功能點呢?
實現之前,我們來了解一下,new
操作符在呼叫建構函式的時候,會進行一個什麼樣的過程:
建立一個全新的物件
這個物件被執行
[[Prototype]]
連線將這個物件繫結到建構函式中的
this
如果函式沒有返回其他物件,則
new
操作符呼叫的函式則會返回這個物件
這可以看出,在 new
執行過程中的第三步,會對函式呼叫的 this
進行修改。在我們簡易版的 bind
函式裡,原函式呼叫中的 this
永遠執行指定的物件,而不能根據如果是 new
呼叫而繫結到 new
建立的物件。所以,我們要對原函式的呼叫進行判斷,是否是 new
呼叫。我們再對簡易版 bind
函式進行修改:
Function.prototype.myBind = function(oThis) {
if(typeof this !== 'function') {
return;
}
var self = this,
args = Array.prototype.slice.call(arguments, 1),
fBound = function () {
return self.apply(
// 檢測是否是 new 建立
(this instanceof self ? this : oThis),
args.concat(Array.prototype.slice.call(arguments))
);
};
// 思考下為什麼要連結原型?提示:如果不連線,上面的檢測是否會成功
if(this.prototype) {
fBound.prototype = this.prototype;
}
return fBound;
}
// 測試
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複製程式碼
經過修改之後,此時我們發現, myBind
函式已經實現原生 bind
函式的功能。在上述程式碼中,留下一個問題,在這裡講一下:
首先,變數
bar
是繫結之後的函式,也就是fBound
。self
是原函式foo
的引用。對於
fBound
函式中的this
的指向,如果是bar('Jack')
這樣直接呼叫,this
指向全域性變數或者undefined
(視是否在嚴格模式下)。但是如果是new bar('Alice')
,根據上面給出的new
執行過程,我們知道,fBound
函式中的this
會指向new
表示式返回的物件,即alice
。捋清楚變數之後,我們接著分析。我們首先忽略掉原型連線,也即忽略
fBound.prototype = this.prototype
這行程式碼。如果是直接呼叫
bar('Jack')
,this instanceof self ? this : oThis
這句判斷,根據上述變數分析,所以此判斷為false
,繫結函式的this
指向oThis
,也即是指定的this
物件。如果是
new
呼叫繫結函式,此時繫結函式中的this
是由new
呼叫繫結函式返回的例項物件,這個物件的建構函式是fBound
,當我們忽略掉原型連線那行程式碼時,其原型物件並不等於原函式self
的原型,所以this instanceof self ? this : oThis
得到的值還是指定的物件,而不是new
返回的物件。所以,知道為什麼要在繫結的時候,繫結函式要與原函式進行原型連線了吧?每次繫結的時候,將繫結函式
fBound
的原型指向原函式的原型,如果new
呼叫繫結函式,得到的例項的原型,也是原函式的原型。這樣在new
執行過程中,執行繫結函式的時候對this
的判斷就可以判斷出是否是new
操作符呼叫
好了,到這基本結束了。
哦,是麼?
等等,在原型連線的時候,你們是否發現 fBound.prototype = this.prototype
這賦值是有問題的?
哦,對哦。
當繫結函式直接連線原函式的原型的時候,如果 fBound
的原型有修改時,是不是原函式的原型也會受到影響了?所以,為了解決這個問題,我們需要一個空函式,作為中間人。
Function.prototype.bind = function(oThis) {
if (typeof this !== 'function') {
// closest thing possible to the ECMAScript 5
// internal IsCallable function
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var aArgs = Array.prototype.slice.call(arguments, 1),
fToBind = this,
// 空函式
fNOP = function() {},
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 獲取呼叫時(fBound)的傳參.bind 返回的函式入參往往是這麼傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關係
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};複製程式碼
上述程式碼是 MDN 提供 bind
函式的 Polyfill
方案,裡面的細節我們都分析完畢了,到這基本理解 bind
函式實現的功能的背後了。
主要的知識點:
this
的繫結規則new
操作符執行過程原型
參考書籍:
《你不知道的 JavaScript》(上卷)