本文主要詳細分解bind函式的polyfill實現。
覺得還行的老闆們可以給個gayhub的satr:github.com/flyFatSeal/…
一:bind基礎
bind函式的具體功能與apply,call函式相似都是改變函式體內的this物件,也就是擴充函式作用域。在MDN中是這樣介紹bind的
bind()方法建立一個新的函式, 當這個新函式被呼叫時其this置為提供的值,其引數列表前幾項置為建立時指定的引數序列。
由上可知,bind函式和apply,call函式不同在於bind函式執行後返回的是一個繫結了this物件的函式,而apply和call函式是直接執行
下面我們來看一個簡單的例子用來說明apply,call和bind的區別: 例 1:
var x = 'out'
var a = {
x:'inner',
func:function(){
console.info('現在的所在的環境是',this.x)
}
}
var b = a.func// inner
b.apply(a) // 現在的所在的環境是inner
b.call(a) // 現在的所在的環境是inner
b.bind(a) // 沒有輸出因為bind函式返回的是一個新的函式
typeof b.bind(a) === 'function' // true
b.bind(a)() // 現在的所在的環境是inner
複製程式碼
因此bind函式可以非同步執行,這是它區別於apply和bind的主要地方。
二:詳解polyfill
bind方法是ECMAScript 5才加入的新方法,因此存在著瀏覽器相容性問題,在具體執行中最好加入polyfill增加相容性。在MDN的polyfill是這樣的
if (!Function.prototype.bind) {
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() {
// this instanceof fNOP === true時,說明返回的fBound被當做new的建構函式呼叫
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是fNOP的例項,因此
// 返回的fBound若作為new的建構函式,new生成的新物件作為this傳入fBound,新物件的__proto__就是fNOP的例項
fBound.prototype = new fNOP();
return fBound;
};
}
複製程式碼
剛看到MDN的polyfill時,基本處於懵逼狀態,後面細細琢磨才明白,要清晰bind的polyfill需要了解bind方法的操作流程,new操作符,和繼承原理才行。
主要疑惑:
- bind方法的polyfill思路是什麼
- 為什麼要this為function物件
- 如何將外部的引數傳入
- fNOP起到了什麼作用
- 為何在fToBind.apply時要判斷對bind呼叫是否是new操作符
bind方法的polyfill思路
通過前面的介紹,bind方法與apply,call不同在於bind方法呼叫時返回的是一個新函式,而apply,call是立即執行,在不支援bind的瀏覽器環境下,需要用apply來模擬bind執行,核心在於bind是返回一個繫結this物件的函式,因此在polyfill中只需要返回一個函式,在返回的函式中通過apply方法繫結this物件和處理引數即可。
this型別判斷
bind方法返回的是一個繫結了this物件的函式,並且bind是Function的方法,在函式體上呼叫,因此要對bind呼叫時的this進行判斷如果不是function物件則丟擲錯誤。
if (typeof this !== 'function') {
// 判斷呼叫bind方法的是否是函式。
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
複製程式碼
引數處理
在bind方法呼叫時需要傳入兩個引數,第一個是繫結的this物件,第二個是繫結的this物件的引數,因為bind方法返回一個新的函式,新的函式又可以傳遞引數。 所以在bind中一共有兩個地方的引數需要處理,呼叫bind方法時的引數,和bind方法呼叫返回新函式,新函式在執行時傳入的引數,這樣說有點抽象,來看一個例子。
var price = function (a,b){
//price.bind(obj,10) obj的value屬性值
console.log('繫結this物件的價格是',this.value)
//price.bind(obj,10)第二個引數的值
console.log('呼叫bind方法傳入的價格是',a)
//price.bind(obj,10)(20)呼叫bind方法執行後函式傳入的引數
console.log('呼叫bind方法執行後的函式傳入的價格是',b)
}
var obj = {
value:5
}
price.bind(obj,10)(20)
//繫結this物件的價格是 5
//呼叫bind方法傳入的價格是 10
//呼叫bind方法執行後的函式傳入的價格是 20
複製程式碼
回到具體的polyfill中,其中
var aArgs = Array.prototype.slice.call(arguments, 1)
這裡的aArgs變數就是用來儲存bind方法呼叫時傳入的引數,其中通過Array.prototype.slice.call可以把傳入的引數轉化為陣列,為什麼要轉化為陣列,因為在具體呼叫bind時,引數個數是不確定的,在不確定引數個數時需要使用apply方法,apply方法的第二引數接受一個陣列。而...slice.call(arguments, 1),則是把bind方法呼叫時傳入的引數從第二個開始轉化為陣列(因為bind方法的第一個引數是繫結的this物件)。
注意這裡處理的是bind(this,arguments)中的arguments,還有bind方法執行後的函式再呼叫時傳入的引數需要處理也就是bind(this,arguments)(fArgs)中的fArgs。
在polyfill中,是這樣處理fArgs的
fBound = function() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 獲取呼叫時(fBound)的傳參.bind 返回的函式入參往往是這麼傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
複製程式碼
在返回函式中通過aArgs.concat(Array.prototype.slice.call(arguments)))一起把bind方法傳入的引數(aArgs)和fArgs組合成為一個新的陣列,作為apply方法的第二個引數。
fNOP函式解析
在polyfill中,fNOP作用類似於寄生組合繼承中的object.create()。作為一箇中間函式連結返回的新函式和原函式的原型鏈。也就是繼承原函式的方法和原型鏈。主要處理當返回的fBound被作為new的建構函式時原型鏈的繼承的情況,注意當這種情況發生時bind方法傳入繫結的this被忽略,引數傳遞不變,this使用原函式的this物件。
fNOP = function() {},
fBound = function() {
return fToBind.apply();
};
// 維護原型關係
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
// 下行的程式碼使fBound.prototype是fNOP的例項,因此
// 返回的fBound若作為new的建構函式,new生成的新物件作為this傳入fBound,新物件的__proto__就是fNOP的例項
fBound.prototype = new fNOP();
return fBound;
複製程式碼
為何要處理new func.bind()這種情況呢?來看一下new操作的執行過程
new操作符解釋引用 sunshine小小倩這篇文章
var a = new myFunction("Li","Cherry");
new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype;
var result = myFunction.call(obj,"Li","Cherry");
return typeof result === 'obj'? result : obj;
}
複製程式碼
1.建立一個空物件 obj;
2.將新建立的空物件的隱式原型指向其建構函式的顯示原型。
3.使用 call 改變 this 的指向
4.如果無返回值或者返回一個非物件值,則將 obj 返回作為新物件;如果返回值是一個新物件的話那麼直接直接返回該物件。
可知在執行new操作時this的指向已經被改變了如果此時還是使用bind方法傳入的要繫結的this,那麼原函式的原型鏈就會被切斷,導致new出來的新物件無法繼承原函式的方法。所以當fToBind被當做建構函式使用時,放棄繫結傳入的this物件。
總結
由上可知,bind的polyfill主要處理了bind方法呼叫時引數傳遞問題,被當做建構函式使用時的繼承問題,如果對bind執行流程和繼承原理熟悉,bind的polyfill就可以一眼看穿了。