我以為我實(shí)現(xiàn)了bind

上一次跳槽面試的時(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)歷娘扩。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末着茸,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子琐旁,更是在濱河造成了極大的恐慌涮阔,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灰殴,死亡現(xiàn)場(chǎng)離奇詭異敬特,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)牺陶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門伟阔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人掰伸,你說我怎么就攤上這事皱炉。” “怎么了狮鸭?”我有些...
    開封第一講書人閱讀 165,417評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵合搅,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我歧蕉,道長(zhǎng)灾部,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評(píng)論 1 295
  • 正文 為了忘掉前任惯退,我火速辦了婚禮赌髓,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘催跪。我一直安慰自己锁蠕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,892評(píng)論 6 392
  • 文/花漫 我一把揭開白布叠荠。 她就那樣靜靜地躺著匿沛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪榛鼎。 梳的紋絲不亂的頭發(fā)上逃呼,一...
    開封第一講書人閱讀 51,692評(píng)論 1 305
  • 那天鳖孤,我揣著相機(jī)與錄音,去河邊找鬼抡笼。 笑死苏揣,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的推姻。 我是一名探鬼主播平匈,決...
    沈念sama閱讀 40,416評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼藏古!你這毒婦竟也來了增炭?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,326評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤拧晕,失蹤者是張志新(化名)和其女友劉穎隙姿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體厂捞,經(jīng)...
    沈念sama閱讀 45,782評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡输玷,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,957評(píng)論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了靡馁。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片欲鹏。...
    茶點(diǎn)故事閱讀 40,102評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赔嚎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胧弛,我是刑警寧澤尽狠,帶...
    沈念sama閱讀 35,790評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站叶圃,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏践图。R本人自食惡果不足惜掺冠,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,442評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望码党。 院中可真熱鬧德崭,春花似錦、人聲如沸揖盘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兽狭。三九已至憾股,卻和暖如春鹿蜀,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背服球。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評(píng)論 1 272
  • 我被黑心中介騙來泰國打工茴恰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人斩熊。 一個(gè)月前我還...
    沈念sama閱讀 48,332評(píng)論 3 373
  • 正文 我出身青樓往枣,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親粉渠。 傳聞我的和親對(duì)象是個(gè)殘疾皇子分冈,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,044評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容