上一次跳槽面試的時(shí)候,一次面試接近尾聲噪舀,進(jìn)行的特別順利魁淳,直到面試官提出一個(gè)問題,“請(qǐng)你實(shí)現(xiàn)一下bind”与倡。
“什么=绻洹!實(shí)現(xiàn)bind纺座?為什么不問call息拜、apply、bind的使用及區(qū)別净响,這些我都倒背如流”少欺。
因?yàn)槟菚r(shí)的段位還很低,對(duì)知識(shí)的掌握還停留在使用層面馋贤,所以被問到的時(shí)候是特別懵的赞别。好在面試官人很好,經(jīng)過多次提示還是寫出了一個(gè)初級(jí)實(shí)現(xiàn)配乓。
代碼如下:
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this // this就是調(diào)用的函數(shù)
// 將arguments轉(zhuǎn)換為數(shù)組
var argsArray = Array.prototype.slice.call(arguments)
// 返回一個(gè)函數(shù)仿滔,符合bind的特性
return function () {
// 返回的函數(shù)中執(zhí)行調(diào)用的函數(shù),并通過apply改變this指向犹芹,傳遞參數(shù)
return me.apply(that, argsArray.slice(1))
}
}
// 驗(yàn)證一下
function aa(p1, p2) {
console.log(this.a + "|" + p1 + "|" + p2)
}
var fn = aa.bind({ a: 2 }, "p1")
fn("p2") // 2|p1|undefined
這就是一個(gè)最最基礎(chǔ)的實(shí)現(xiàn)崎页,我一度認(rèn)為bind的實(shí)現(xiàn)也不過如此。不過在使用的時(shí)候發(fā)現(xiàn)一個(gè)問題羽莺,注意上面的驗(yàn)證代碼輸出結(jié)果实昨,undefined
是什么鬼?不應(yīng)該是輸出 2|p1|p2
盐固。這時(shí)因?yàn)槿绱藢?shí)現(xiàn)的bind只能通過調(diào)用bind的時(shí)候給函數(shù)傳參荒给,無法在調(diào)用bind返回的函數(shù)時(shí)傳參。
有了上面的實(shí)現(xiàn)刁卜,解決這個(gè)問題也不太難志电。
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this
var args = Array.prototype.slice.call(arguments, 1)
return function () {
// 獲取調(diào)用返回的函數(shù)時(shí)傳遞的參數(shù),并將兩次參數(shù)合并
var innerArgs = Array.prototype.slice.call(arguments)
var totalArgs = args.concat(innerArgs)
return me.apply(that, totalArgs)
}
}
// 驗(yàn)證一下
function aa(p1, p2) {
console.log(this.a + "|" + p1 + "|" + p2)
}
var fn = aa.bind({ a: 2 }, "p1")
fn("p2") // 2|p1|p2
驗(yàn)證通過蛔趴,心想這回應(yīng)該沒有問題了吧挑辆。直到有一天在總結(jié) this 指向問題的時(shí)候。遇到了一個(gè) new 和 bind 同時(shí)出現(xiàn)的情況。也就是說當(dāng) bind 返回的函數(shù)作為構(gòu)造函數(shù)調(diào)用時(shí)鱼蝉。那么通過 bind 綁定的this就需要被忽略洒嗤,很明顯 this 要綁定到創(chuàng)建的實(shí)例上。
從改變 this 指向的角度來說魁亦,new 的優(yōu)先級(jí)要高于 bind 的綁定渔隶。
如果對(duì)this的指向問題感興趣可以參考《this到底指向誰》一文。
知道真相的我趕緊翻出代碼洁奈,完善我的 bind间唉。這次的進(jìn)展不如上次順利,因?yàn)橛忠玫嚼^承的相關(guān)知識(shí)利术。抽出時(shí)間又將js的繼承簡(jiǎn)單總結(jié)了下呈野,《永不過時(shí)的面向?qū)ο蟆^承》。這下算是豁然開朗印叁,噼里啪啦……代碼如下:
Function.prototype.bind = Function.prototype.bind || function (that) {
var me = this
var args = Array.prototype.slice.call(arguments, 1)
var F = function () { }
// F的原型繼承調(diào)用函數(shù)的原型被冒,利用空對(duì)象方式實(shí)現(xiàn)原型鏈繼承
F.prototype = this.prototype
var bound = function () {
var innerArgs = Array.prototype.slice.call(arguments)
var totalArgs = args.concat(innerArgs)
return me.apply(this instanceof F ? this : that, totalArgs)
}
// 將 bound 的 prototype 對(duì)象指向一個(gè) F 的實(shí)例
bound.prototype = new F()
return bound
}
核心在于通過創(chuàng)建空對(duì)象的方式,實(shí)現(xiàn)了 bound 繼承調(diào)用函數(shù)喉钢。
為何要繼承原函數(shù)姆打?
因?yàn)?new 調(diào)用 bind 后返回的函數(shù),也是相當(dāng)于將原函數(shù)作為構(gòu)造函數(shù)調(diào)用肠虽,創(chuàng)建實(shí)例,如果不繼承原函數(shù)玛追,那么創(chuàng)建的實(shí)例與原函數(shù)沒有任何關(guān)系税课。
另一個(gè)關(guān)鍵點(diǎn)在于對(duì) new 的理解,new 的時(shí)候都做了些什么操作痊剖,在上面分享的《繼承》一文中有詳細(xì)解答韩玩。
由于通過 new 調(diào)用返回的函數(shù)時(shí),bound 內(nèi)的 this 指向自身實(shí)例陆馁。并且 bound 的原型指向 F 的實(shí)例找颓,又因?yàn)?F 的原型繼承調(diào)用函數(shù)的原型,所以有 this instanceof F
為 true叮贩,自然三目表達(dá)式的結(jié)果為 this击狮。因此創(chuàng)建的實(shí)例也是調(diào)用函數(shù)的實(shí)例。this instanceof me
也為 true益老。
驗(yàn)證代碼:
function Animal(a) {
this.a = a
}
const o1 = {}
// 普通調(diào)用
var a1 = Animal.bind(o1)
a1(2)
console.log(o1.a) // 2
// 作為構(gòu)造函數(shù)調(diào)用
var a2 = new (Animal.bind(o1))(5);
console.log(a2) // Animal {a: 5}
a2.__proto__.constructor === Animal // true
console.log(a2.a) // 5
這次我不敢說我實(shí)現(xiàn)了 bind彪蓬,只能說這次的實(shí)現(xiàn)比之前更完善。為了看下 bind 的完美實(shí)現(xiàn)方式捺萌,翻出了es5-shim.js中的 bind 源碼档冬。
function bind(that) {
var target = this;
if (!isCallable(target)) {
throw new TypeError('Function.prototype.bind called on incompatible ' + target);
}
var args = array_slice.call(arguments, 1);
var bound;
var binder = function () {
if (this instanceof bound) {
// 構(gòu)造函數(shù)調(diào)用情況
var result = apply.call(
target,
this,
array_concat.call(args, array_slice.call(arguments))
);
if ($Object(result) === result) {
return result;
}
return this;
} else {
// 正常調(diào)用情況
return apply.call(
target,
that,
array_concat.call(args, array_slice.call(arguments))
);
}
};
var boundLength = max(0, target.length - args.length);
var boundArgs = [];
for (var i = 0; i < boundLength; i++) {
array_push.call(boundArgs, '$' + i);
}
bound = $Function(
'binder',
'return function (' + array_join.call(boundArgs, ',') + '){ return binder.apply(this, arguments); }'
)(binder);
if (target.prototype) {
Empty.prototype = target.prototype;
bound.prototype = new Empty();
Empty.prototype = null;
}
return bound;
}
比我想象的要復(fù)雜一些,但是實(shí)現(xiàn)的核心部分是相似的。其中有一點(diǎn)是特別容易被忽略的酷誓,就是每個(gè)函數(shù)都有像數(shù)組和字符串那樣的 length 屬性披坏,用于表示函數(shù)的形參個(gè)數(shù)。并且函數(shù)的 length 屬性值是不可重寫的盐数。es5-shim 是為了最大限度地進(jìn)行兼容棒拂,包括對(duì)返回函數(shù) length 屬性的還原。
一次 bind 實(shí)現(xiàn)的經(jīng)歷娘扩。