最近的面試中被問到了 bind 這個方法,并寫出其 Polyfill,感覺回答的不太好,在此重新整理一下。
Function.prototype.bind 是 ES5 引入的方法,會返回一個新函數(shù)并修改函數(shù)的 this 指向,舉個例子(摘自 MDN):
this.x = 9;
let module = {
x: 81,
getX() {
return this.x;
}
};
module.getX(); // 返回81,this指向module
let retrieveX = module.getX;
retrieveX(); // 返回9,this指向全局作用域
// 創(chuàng)建一個新函數(shù),將this綁定到module對象
let boundGetX = retrieveX.bind(module);
boundGetX(); // 返回81,this指向module
所以,可以容易得出:
Function.prototype.bind = function (thisArg) {
var self = this;
return function() {
return self.apply(thisArg, arguments);
}
}
注意到 bind 方法的定義如下:
fun.bind(thisArg[, arg1[, arg2[, ...]]])
參數(shù)
thisArg
當綁定函數(shù)被調(diào)用時,該參數(shù)會作為原函數(shù)運行時的this指向。當使用new操作符調(diào)用綁定函數(shù)時,該參數(shù)無效。
arg1, arg2, ...
當綁定函數(shù)被調(diào)用時,這些參數(shù)將置于實參之前傳遞給被綁定的方法。
因此,bind 除了可以綁定 this 指向外,還可以綁定調(diào)用參數(shù)。代碼修改如下:
Function.prototype.bind = function (thisArg) {
// 借用 Array 的 slice 方法去掉第一個參數(shù) thisArg,剩下的是函數(shù)調(diào)用參數(shù)
var bindArgs = Array.prototype.slice.call(arguments, 1);
var self = this;
return function() {
return self.apply(thisArg, bindArgs.concat(arguments));
}
}
此外,還需要考慮到綁定的函數(shù)可以是構(gòu)造函數(shù),然而返回的新函數(shù)不具有綁定函數(shù)的原型鏈,因而需要修復原型鏈。代碼修改如下:
Function.prototype.bind = function (thisArg) {
// 借用 Array 的 slice 方法去掉第一個參數(shù) thisArg,剩下的是函數(shù)調(diào)用參數(shù)
var bindArgs = Array.prototype.slice.call(arguments, 1);
var self = this;
var bindFunction = function() {
return self.apply(thisArg, bindArgs.concat(arguments));
}
// 引入空函數(shù) F,避免原型鏈上引用類型屬性共享
function F() {}
F.prototype = this.prototype;
bindFunction.prototype = new F();
// 修復 constructor 屬性
bindFunction.prototype.constructor = this;
return bindFunction;
}
至此完成了 Function.prototype.bind 的 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() {
return fToBind.apply(this instanceof fNOP
? this
: oThis,
// 獲取調(diào)用時(fBound)的傳參,bind 返回的函數(shù)入?yún)⑼沁@么傳遞的
aArgs.concat(Array.prototype.slice.call(arguments)));
};
// 維護原型關(guān)系
if (this.prototype) {
// Function.prototype doesn't have a prototype property
fNOP.prototype = this.prototype;
}
fBound.prototype = new fNOP();
return fBound;
};
}
仍然存在的問題:
- 這部分實現(xiàn)依賴于
Array.prototype.slice(),Array.prototype.concat(),Function.prototype.call()這些原生方法。 - 這部分實現(xiàn)創(chuàng)建的函數(shù)的實現(xiàn)并沒有
caller以及會在get,set或者deletion上拋出TypeError錯誤的arguments屬性這兩個不可改變的“毒藥” 。(假如環(huán)境支持Object.defineProperty,或者實現(xiàn)支持__defineGetter__和__defineSetter__擴展) - 這部分實現(xiàn)創(chuàng)建的函數(shù)有
prototype屬性。(正確的綁定函數(shù)沒有的) - 這部分實現(xiàn)創(chuàng)建的綁定函數(shù)所有的
length屬性并不是同ECMA-262標準一致的:它的length是 0,而在實際的實現(xiàn)中根據(jù)目標函數(shù)的length和預先指定的參數(shù)個數(shù)可能會返回非零的length。
Reference
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind