JavaScript深入理解系列:手寫bind方法

定義

bind() 方法創(chuàng)建一個新的函數(shù)窥突,在 bind() 被調(diào)用時吵聪,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù)胧沫,供調(diào)用時使用荐健。

由定義可知,函數(shù)調(diào)用bind()方法的時候琳袄,會返回一個新函數(shù),并且this指向bind函數(shù)的第一個參數(shù)纺酸,簡單來表示窖逗。

fn.bind(obj對象)執(zhí)行 返回一個函數(shù),想調(diào)用的時候餐蔬,fn.bind(obj)()這樣來執(zhí)行;

舉個例子:

var name = '炒米粉';
var obj = {
    name: '程序員米粉'
};
function fn() {
    console.log(this.name);
}

var getFn = fn.bind(obj); // 返回一個函數(shù)命名getFn
getFn(); // this.name => '程序員米粉'碎紊, this指向obj
fn(); // this.name => '炒米粉', this指向window

總結(jié):

1樊诺、bind()執(zhí)行仗考,返回一個函數(shù)。
2词爬、bind()接受多個參數(shù)秃嗜。

第1步:模擬返回一個函數(shù)

由上述定義以及總結(jié)可知,我們首先模擬一個函數(shù)調(diào)用bind()方法返回一個函數(shù):

Function.prototype.myBind = function(context) {
    var context = context || window;
    var self = this;
    return function() {
        self.apply(context);
    };
};

// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數(shù)命名getFn
getFn(); // this.name => '程序員米粉', this指向obj

apply實現(xiàn)可以看我以往寫的 《javascript深入系列之手寫call與apply》

那我們把fn再改造一下锅锨,加一個返回值

function fn() {
    console.log(this.name);
    return 1
}
// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數(shù)命名getFn
console.log(getFn()); // 發(fā)現(xiàn)并沒有把目標(biāo)值1返回回來

上面的例子getFn()執(zhí)行發(fā)現(xiàn)并沒有把目標(biāo)值1返回回來叽赊,那我們需要myBind函數(shù)里面添加 return self.apply(context);

最終代碼:

Function.prototype.myBind = function(context) {
    var context = context || window;
    var self = this;
    return function() {
        // 這里加了return
        return self.apply(context);
    };
};
// 使用一下
var getFn = fn.myBind(obj); // 返回一個函數(shù)命名getFn
console.log(getFn()); // 1, 目標(biāo)值達到

第2步:模擬返回接受多個參數(shù)

我們先來看看bind是怎樣接收多個參數(shù)的。還是原來的例子:

var name = '炒米粉';
var obj = {
    name: '程序員米粉'
};
function fn(age, weight) {
    console.log(this.name);
    console.log(age, weight);
}

// var getFn = fn.bind(obj, 18, 150); 
// getFn(); // this.name => '程序員米粉'必搞,age, weight => 18, 150

// var getFn = fn.bind(obj); 
// getFn( 18, 150); // this.name => '程序員米粉'必指,age, weight => 18, 150

var getFn = fn.bind(obj, 18); 
getFn(150); // this.name => '程序員米粉',age, weight => 18, 150

從例子可以看出來恕洲,參數(shù)可以在兩個地方傳進來塔橡。在用bind綁定函數(shù)fn的時候傳參數(shù)18, fn.bind(obj, 18); 返回函數(shù)的時候傳參數(shù)15 getFn(150)霜第。

思考一下葛家,我們?nèi)绾伟堰@兩個地方傳進來的參數(shù)合并一下。別問庶诡,我們可以用arguments對象獲取傳傳進來的參數(shù)合并一下惦银。那我們繼續(xù)修改一下代碼:

Function.prototype.myBind = function(context) {
    var context = context || window;
    var self = this;
     // myBind函數(shù)傳進來的參數(shù),從第2個開始取 
    var args = arguments.length && Array.prototype.slice.call(1, arguments.length) || [];
     // array.slice(start, end)
    return function() {
        // 返回函數(shù)傳進來的參數(shù)末誓,從第1個開始取 
        var arr = arguments.length && Array.prototype.slice.call(0, arguments.length) || [];
        return self.apply(context, arr.concat(args));
    };
};

var getFn = fn.bind(obj, 18); 
getFn(150); // age, weight => 18, 150

好了我們已經(jīng)搞定了總結(jié)里面的兩點了扯俱。

第3步:模擬構(gòu)造函數(shù)效果

bind()方法還有一種特殊的用法,那就是原函數(shù)使用bind()方法綁定函數(shù)之后喇澡,原函數(shù)可以作為構(gòu)造函數(shù)使用的迅栅。

綁定函數(shù)自動適應(yīng)于使用 new 操作符去構(gòu)造一個由目標(biāo)函數(shù)創(chuàng)建的新實例。當(dāng)一個綁定函數(shù)是用來構(gòu)建一個值的晴玖,原來提供的 this 就會被忽略读存。 --來源moz

這什么意思呢?那我們先來代碼看一下:

var name = '炒米粉';
var obj = {
    name: '程序員米粉'
};
function fn(age, weight) {
    console.log(this.name);
    console.log(age, weight);
    this.phoneNumber = '188888888'
}

fn.prototype.haveMoney = 'NO';
var getFn = fn.bind(obj, 18);
// 通過使用bind()方法返回的綁定函數(shù)呕屎,可以使用new創(chuàng)建實例對象让簿。fn的this不再指向obj,而是新創(chuàng)建的對象newObj
var newObj = new getFn(150); 
// fn函數(shù)
// this.name => undefined;
// age, weight => 18, 50
newObj.haveMoney // => 'NO'
newObj.phoneNumber // => '188888888'

由上面的例子可以看出來this.name變成了undefined秀睛,為什么呢尔当?由于使用了new創(chuàng)建了新對象newObjthis.name中的this失效了蹂安,this指向了newObj椭迎,如果沒有用new創(chuàng)建新對象,this是指向bind(obj)中的obj的田盈。原函數(shù)fn就作為了一個構(gòu)造函數(shù)使用畜号,所以newObj作為new創(chuàng)建出來的實例,可以繼承fn的原型上的屬性和方法允瞧,可見 newObj.haveMoney // => 'NO',newObj.phoneNumber // => '188888888'

繼續(xù)改進一下我們myBind方法:

var name = '炒米粉';
var obj = {
    name: '程序員米粉'
};
function fn(age, weight) {
    console.log(this.name);
    console.log(age, weight);
    this.phoneNumber = '188888888';
}

fn.prototype.haveMoney = 'NO';

Function.prototype.myBind = function(context) {
    var context = context || window;
    var self = this;
    // myBind函數(shù)傳進來的參數(shù)简软,從第2個開始取
    var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
    var returnFn = function() {
        var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
        // this instanceof returnFn 什么意思蛮拔?這是一個判斷對象是否屬于繼承于當(dāng)前對象
        // 如果為true,那就是返回的函數(shù)returnFn作為了一個構(gòu)造函數(shù)使用替饿,那么this指向新實例语泽,不再是context
        // 如果為false,那就是返回的函數(shù)returnFn作為了一個普通函數(shù)使用视卢,那么this指向context或者window

        return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
    };
    // 這樣寫踱卵,使returnFn作為一個構(gòu)造函數(shù)使用的時候,那么通過原型繼承的方法据过,returnFn可以繼承當(dāng)前對象里面原型上面所有屬性和方法
    // 修正prototype之后惋砂,創(chuàng)建的新實例,可以繼承原型的屬性和方法绳锅,例如例子西饵,實例newObj繼承了,this.phoneNumber中的屬性值鳞芙。
    returnFn.prototype = this.prototype;
    return returnFn;
};

var getFn = fn.myBind(obj, 18);
var newObj = new getFn(150);
// console.log(this.name); => undefined;
// console.log(age, weight); => 18, 50
newObj.haveMoney // => 'NO'
newObj.phoneNumber // => '188888888'

上面例子比較難懂就是this instanceof returnFn ? this : contextreturnFn.prototype = this.prototype;眷柔,這都是跟js里面的原型繼承相關(guān)知識點。

第4步:最終版本

好了原朝,我們已經(jīng)完成了90%了驯嘱,但是還有點小瑕疵,關(guān)于原型繼承的喳坠,我們先來看看例子:

function fn() {}
Function.prototype.myBind = function(context) {
    var context = context || window;
    var self = this;
    // myBind函數(shù)傳進來的參數(shù)鞠评,從第2個開始取
    var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];

    var returnFn = function() {
        var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
        // this instanceof returnFn 什么意思?這是一個判斷對象是否屬于繼承于當(dāng)前對象
        // 如果  為true壕鹉,那就是返回的函數(shù)returnFn作為了一個構(gòu)造函數(shù)使用剃幌,那么this指向新實例,不再是context
        // 如果 為false晾浴,那就是返回的函數(shù)returnFn作為了一個普通函數(shù)使用负乡,那么this指向context或者window

        return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
    };
    // 這樣寫,使returnFn作為一個構(gòu)造函數(shù)使用的時候脊凰,那么通過原型繼承的方法敬鬓,returnFn可以繼承當(dāng)前對象里面原型上面所有屬性和方法
    // 修正prototype之后,創(chuàng)建的新實例笙各,可以繼承原型的屬性和方法,例如例子础芍,實例newObj繼承了杈抢,this.phoneNumber中的屬性值。
    returnFn.prototype = this.prototype;
    return returnFn;
};
var getFn = fn.myBind(obj);
getFn.prototype.text = '我是被bind方法返回的函數(shù)'
console.log(fn.prototype.text) // '我是被bind方法返回的函數(shù)'仑性,理論上這里不應(yīng)該被修改惶楼,應(yīng)該為undefined

由上述例子可以看出來,我們只是修改了通過用了bind方法返回函數(shù)的prototype對象添加了屬性,那么原函數(shù)的原型對象prototype也被修改了歼捐。這樣是不行的何陆。所以我們繼續(xù)優(yōu)化了方法。

function fn() {}
Function.prototype.myBind = function(context) {
    if (typeof this !== 'function') {
        throw new Error('The bind method is not available');
    }
    var context = context || window;
    var self = this;
    // myBind函數(shù)傳進來的參數(shù)豹储,從第2個開始取
    var args = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];

    var returnFn = function() {
        var arr = (arguments.length && Array.prototype.slice.call(1, arguments.length)) || [];
        return self.apply(this instanceof returnFn ? this : context, arr.concat(args));
    };

    var templateFn = function() {};
    // 修正prototype之后贷盲,templateFn可以繼承原函數(shù)原型的屬性和方法
    templateFn.prototype = this.prototype;
    // 創(chuàng)建空函數(shù)實例,然后給returnFn的原型繼承剥扣,returnFn創(chuàng)建的實例可以繼承templateFn方法巩剖。
    // 這樣修改了returnFn原型方法,都影響不了 原函數(shù)myBind原型對象钠怯,只會影響空函數(shù)templateFn佳魔。妙妙妙啊。
    returnFn.prototype = new templateFn();
    return returnFn;
};

var getFn = fn.myBind(obj);
var newObj = new getFn();
getFn.prototype.text = '我是被bind方法返回的函數(shù)'
console.log(fn.prototype.text) // => undefined

上述例子晦炊,可以看出來我們通過一個空函數(shù) var templateFn = function() {};鞠鲜,那么空函數(shù)templateFn的原型繼承原函數(shù)myBind的原型對象,再把返回函數(shù)returnFn繼承空函數(shù)templateFn的實例屬性和方法断国。這樣子改變返回函數(shù)templateFn的原型對象贤姆,不再影響原函數(shù)了myBind原型對象,簡直一個妙字并思。

防止調(diào)用不是函數(shù)情況

if (typeof this !== 'function') {
        throw new Error('The bind method is not available');
    }

題外話:apply庐氮、call、bind三者的區(qū)別

  • 三者都可以改變函數(shù)的this對象指向宋彼。

  • 三者第一個參數(shù)都是this要指向的對象弄砍,如果沒有這個參數(shù)或參數(shù)為undefined或null,則默認(rèn)指向全局window输涕。

  • 三者都可以傳參音婶,但是apply是數(shù)組,而call是參數(shù)列表莱坎,且apply和call是一次性傳入?yún)?shù)衣式,而bind可以分為多次傳入,bind是返回綁定this之后的函數(shù)檐什,apply碴卧、call 則是立即執(zhí)行。

結(jié)語

希望看完這篇文章對你有幫助:

  • 理解bind原理
  • 實踐寫一個bind方法
  • 區(qū)分apply乃正、call住册、bind三者的區(qū)別

文中如有錯誤,歡迎在評論區(qū)指正瓮具,如果這篇文章幫助到了你荧飞,歡迎點贊和關(guān)注凡人,后續(xù)會輸出更好的分享。

歡迎關(guān)注公眾號:【程序員米粉】
公眾號分享開發(fā)編程叹阔、職場晉升挠轴、大廠面試經(jīng)驗

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市耳幢,隨后出現(xiàn)的幾起案子岸晦,更是在濱河造成了極大的恐慌,老刑警劉巖帅掘,帶你破解...
    沈念sama閱讀 206,723評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件委煤,死亡現(xiàn)場離奇詭異,居然都是意外死亡修档,警方通過查閱死者的電腦和手機碧绞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來吱窝,“玉大人讥邻,你說我怎么就攤上這事≡合浚” “怎么了兴使?”我有些...
    開封第一講書人閱讀 152,998評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長照激。 經(jīng)常有香客問我发魄,道長,這世上最難降的妖魔是什么俩垃? 我笑而不...
    開封第一講書人閱讀 55,323評論 1 279
  • 正文 為了忘掉前任励幼,我火速辦了婚禮,結(jié)果婚禮上口柳,老公的妹妹穿的比我還像新娘苹粟。我一直安慰自己,他們只是感情好跃闹,可當(dāng)我...
    茶點故事閱讀 64,355評論 5 374
  • 文/花漫 我一把揭開白布嵌削。 她就那樣靜靜地躺著,像睡著了一般望艺。 火紅的嫁衣襯著肌膚如雪苛秕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,079評論 1 285
  • 那天找默,我揣著相機與錄音想帅,去河邊找鬼。 笑死啡莉,一個胖子當(dāng)著我的面吹牛港准,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播咧欣,決...
    沈念sama閱讀 38,389評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼浅缸,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了魄咕?” 一聲冷哼從身側(cè)響起衩椒,我...
    開封第一講書人閱讀 37,019評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哮兰,沒想到半個月后毛萌,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡喝滞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,971評論 2 325
  • 正文 我和宋清朗相戀三年阁将,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片右遭。...
    茶點故事閱讀 38,100評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡做盅,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出窘哈,到底是詐尸還是另有隱情吹榴,我是刑警寧澤,帶...
    沈念sama閱讀 33,738評論 4 324
  • 正文 年R本政府宣布滚婉,位于F島的核電站图筹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏让腹。R本人自食惡果不足惜远剩,卻給世界環(huán)境...
    茶點故事閱讀 39,293評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望哨鸭。 院中可真熱鬧民宿,春花似錦、人聲如沸像鸡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,289評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽只估。三九已至志群,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蛔钙,已是汗流浹背锌云。 一陣腳步聲響...
    開封第一講書人閱讀 31,517評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留吁脱,地道東北人桑涎。 一個月前我還...
    沈念sama閱讀 45,547評論 2 354
  • 正文 我出身青樓彬向,卻偏偏與公主長得像,于是被迫代替她去往敵國和親攻冷。 傳聞我的和親對象是個殘疾皇子娃胆,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,834評論 2 345

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