JavaScript基礎(chǔ)專(zhuān)題之手動(dòng)實(shí)現(xiàn)call、apply屿愚、bind(六)

實(shí)現(xiàn)自己的call

MDN 定義:

call() 提供新的 this 值給當(dāng)前調(diào)用的函數(shù)/方法汇跨。你可以使用 call 來(lái)實(shí)現(xiàn)繼承:寫(xiě)一個(gè)方法,然后讓另外一個(gè)新的對(duì)象來(lái)繼承它(而不是在新對(duì)象中再寫(xiě)一次這個(gè)方法)妆距。

簡(jiǎn)答的概括就是:

call() 方法在使用一個(gè)指定的 this 值和若干個(gè)指定的參數(shù)值的前提下調(diào)用某個(gè)函數(shù)或方法穷遂。

舉個(gè)例子:

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1

簡(jiǎn)單的解析一下call都做了什么:

第一步:call 改變了 this 的指向,指向到 foo

第二步:bar 函數(shù)執(zhí)行

函數(shù)通過(guò) call 調(diào)用后娱据,結(jié)構(gòu)就如下面代碼:

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1

這樣this 就指向了 foo蚪黑,但是我們給foo添加了一個(gè)屬性,這并不可取中剩。所以我們還要執(zhí)行一步刪除的動(dòng)作忌穿。

所以我們模擬的步驟可以分為:

第一步:將函數(shù)設(shè)為傳入對(duì)象的屬性

第二步:執(zhí)行該函數(shù)

第三部:刪除該函數(shù)

以上個(gè)例子為例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn

注意:fn 是對(duì)象的臨時(shí)屬性结啼,因?yàn)閳?zhí)行過(guò)后要?jiǎng)h除滴掠剑。

根據(jù)這個(gè)思路,我們可以嘗試著去寫(xiě)一個(gè)call

Function.prototype._call = function(context) {
    // 首先要獲取調(diào)用call的函數(shù)郊愧,用this可以獲取
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 測(cè)試一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar._call(foo); // 1

OK朴译,我們可以在控制臺(tái)看到結(jié)果了,和預(yù)想的一樣属铁。

這樣只是將第一個(gè)參數(shù)作為上下文進(jìn)行執(zhí)行眠寿,但是并沒(méi)用傳入?yún)?shù),下面我們嘗試傳入?yún)?shù)執(zhí)行焦蘑。

舉個(gè)例子:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call(foo, 'chris', 10);
// chris
// 10
// 1

我們會(huì)發(fā)現(xiàn)參數(shù)并不固定盯拱,所以要在 Arguments 對(duì)象的第二個(gè)參數(shù)截取,傳入到數(shù)組中例嘱。

比如這樣:

// 以上個(gè)例子為例狡逢,此時(shí)的arguments為:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 因?yàn)閍rguments是類(lèi)數(shù)組對(duì)象,所以可以用for循環(huán)
var args = [];
vae len = arguments.length
for(var i = 1,  i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 執(zhí)行后 args為 ["arguments[1]", "arguments[2]", "arguments[3]"]

OK拼卵,看到這樣操作第一反應(yīng)會(huì)想到 ES6 的方法奢浑,不過(guò) call 是 ES3 的方法,所以就麻煩一點(diǎn)吧间学。所以我們這次用 eval 方法拼成一個(gè)函數(shù),類(lèi)似于這樣:

eval('context.fn(' + args +')')

這里 args 會(huì)自動(dòng)調(diào)用 Array.toString() 這個(gè)方法。

代碼如下:

Function.prototype._call = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 測(cè)試一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar._call(foo, 'chris', 10); 
// chris
// 10
// 1

OK低葫,這樣我們實(shí)現(xiàn)了 80% call的功能详羡。

再看看定義:

根據(jù) MDN 對(duì) call 語(yǔ)法的定義:

第一個(gè)參數(shù):

fun 函數(shù)運(yùn)行時(shí)指定的 this需要注意的是嘿悬,指定的 this 值并不一定是該函數(shù)執(zhí)行時(shí)真正的 this 值实柠,如果這個(gè)函數(shù)在非嚴(yán)格模式下運(yùn)行,則指定為 nullundefinedthis 值會(huì)自動(dòng)指向全局對(duì)象(瀏覽器中就是 window 對(duì)象)善涨,同時(shí)值為原始值(數(shù)字窒盐,字符串,布爾值)的 this 會(huì)指向該原始值的自動(dòng)包裝對(duì)象钢拧。

執(zhí)行參數(shù):

使用調(diào)用者提供的 this 值和參數(shù)調(diào)用該函數(shù)的返回值蟹漓。若該方法沒(méi)有返回值,則返回 undefined源内。

所以我們還需要注意兩個(gè)點(diǎn)

1.this 參數(shù)可以傳 null葡粒,當(dāng)為 null 的時(shí)候,視為指向 window

舉個(gè)例子:

var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1

雖然這個(gè)例子本身不使用 call膜钓,結(jié)果依然一樣嗽交。

2.函數(shù)是可以有返回值

舉個(gè)例子:

var obj = {
    value: 1
}

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(obj, 'chris', 10)
// Object {
//    value: 1,
//    name: 'chris',
//    age: 10
// }

不過(guò)都很好解決,讓我們直接看第三版也就是最后一版的代碼:

Function.prototype._call = function (context = window) {
    var context = context;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// 測(cè)試一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar._call(null); // 2

console.log(bar._call(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

這樣我們就成功的完成了一個(gè)call函數(shù)颂斜。

實(shí)現(xiàn)自己的apply

apply 的實(shí)現(xiàn)跟 call 類(lèi)似夫壁,只是后面?zhèn)鞯膮?shù)是一個(gè)數(shù)組或者類(lèi)數(shù)組對(duì)象。

Function.prototype.apply = function (context = window, arr) {
    var context = context;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

實(shí)現(xiàn)自己的bind

根據(jù) MDN 定義:

bind() 方法會(huì)創(chuàng)建一個(gè)新函數(shù)沃疮。當(dāng)這個(gè)新函數(shù)被調(diào)用時(shí)盒让,bind() 的第一個(gè)參數(shù)將作為它運(yùn)行時(shí)的 this,之后的一序列參數(shù)將會(huì)在傳遞的實(shí)參前傳入作為它的參數(shù)忿磅。

由此我們可以首先得出 bind 函數(shù)的三個(gè)特點(diǎn):

  1. 改變this指向
  2. 返回一個(gè)函數(shù)
  3. 可以傳入?yún)?shù)
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

var bindFoo = bar.bind(foo); // 返回了一個(gè)函數(shù)

bindFoo(); // 1

關(guān)于指定 this 的指向糯彬,我們可以使用 call 或者 apply 實(shí)現(xiàn)。

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

之所以是 return self.apply(context) 葱她,是考慮到綁定函數(shù)可能是有返回值的撩扒,依然是這個(gè)例子:

var foo = {
    value: 1
};

function bar() {
    return this.value;
}

var bindFoo = bar.bind(foo);

console.log(bindFoo()); // 1

第三點(diǎn),可以傳入?yún)?shù)吨些。這個(gè)很困惑是在 bind 時(shí)傳參還是在 bind 之后傳參搓谆。

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFoo = bar.bind(foo, 'chris');
bindFoo('18');
// 1
// chris
// 18

通過(guò)實(shí)例,我們發(fā)現(xiàn)兩者參數(shù)是可以累加的豪墅,就是第一次 bind 時(shí)傳的參數(shù)和可以在調(diào)用的時(shí)候傳入泉手。

所以我們還是用 arguments 進(jìn)行處理:

Function.prototype._bind = function (context) {
    var self = this;
    // 獲取_bind函數(shù)從第二個(gè)參數(shù)到最后一個(gè)參數(shù)
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 這個(gè)時(shí)候的arguments是指bind返回的函數(shù)傳入的參數(shù)
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}

完成了上面三步,其實(shí)我們還有一個(gè)問(wèn)題沒(méi)有解決偶器。

根據(jù) MDN 定義:

一個(gè)綁定函數(shù)也能使用new操作符創(chuàng)建對(duì)象:這種行為就像把原函數(shù)當(dāng)成構(gòu)造器斩萌。提供的 this 值被忽略缝裤,同時(shí)調(diào)用時(shí)的參數(shù)被提供給模擬函數(shù)。

舉個(gè)例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'james';

var bindFoo = bar.bind(foo, 'chris');

var obj = new bindFoo('18');
// undefined
// chris
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// james

盡管在全局和 foo 中都聲明了 value 值颊郎,還是返回了 undefind憋飞,說(shuō)明this已經(jīng)失效了,如果大家了解 new 的實(shí)現(xiàn)姆吭,就會(huì)知道this是指向 obj 的榛做。

所以我們可以通過(guò)修改返回的函數(shù)的原型來(lái)實(shí)現(xiàn),讓我們寫(xiě)一下:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 當(dāng)作為構(gòu)造函數(shù)時(shí)内狸,this 指向?qū)嵗烀校藭r(shí)結(jié)果為 true,將綁定函數(shù)的 this 指向該實(shí)例昆淡,可以讓實(shí)例獲得來(lái)自綁定函數(shù)的值
        // 以上面的是 demo 為例锰瘸,如果改成 `this instanceof fBound ? null : context`,實(shí)例只是一個(gè)空對(duì)象瘪撇,將 null 改成 this 获茬,實(shí)例會(huì)具有 habit 屬性
        // 當(dāng)作為普通函數(shù)時(shí),this 指向 window倔既,此時(shí)結(jié)果為 false恕曲,將綁定函數(shù)的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實(shí)例就可以繼承綁定函數(shù)的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}

但是在這個(gè)寫(xiě)法中渤涌,我們直接將 fBound.prototype = this.prototype佩谣,我們直接修改 fBound.prototype 的時(shí)候,也會(huì)直接修改綁定函數(shù)的 prototype实蓬。這個(gè)時(shí)候茸俭,我們可以需要一個(gè)空函數(shù)來(lái)進(jìn)行中轉(zhuǎn):

Function.prototype._bind = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

還存在一些問(wèn)題:

1.調(diào)用 bind 的不是函數(shù)咋辦?

做一個(gè)類(lèi)型判斷唄

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}

2.我要在線上用

做一下兼容性測(cè)試

Function.prototype.bind = Function.prototype.bind || function () {
    ……
};

好了安皱,這樣就我們就完成了一個(gè) bind

Function.prototype._bind = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

補(bǔ)充

eval根據(jù) MDN 定義:表示JavaScript表達(dá)式调鬓,語(yǔ)句或一系列語(yǔ)句的字符串。表達(dá)式可以包含變量以及已存在對(duì)象的屬性酌伊。

一個(gè)簡(jiǎn)單的例子:

var x = 2;
var y = 39;
function add(x,y){
    return x + y
}
eval('add('+ ['x','y'] + ')')//等于add(x,y)

也就說(shuō)eavl調(diào)用函數(shù)后腾窝,字符串會(huì)被解析出變量,達(dá)到去掉字符串調(diào)用變量的目的居砖。

JavaScript基礎(chǔ)系列目錄

JavaScript基礎(chǔ)專(zhuān)題之原型與原型鏈(一)

JavaScript基礎(chǔ)專(zhuān)題之執(zhí)行上下文和執(zhí)行棧(二)

JavaScript基礎(chǔ)專(zhuān)題之深入執(zhí)行上下文(三)

JavaScript基礎(chǔ)專(zhuān)題之閉包(四)

JavaScript基礎(chǔ)專(zhuān)題之參數(shù)傳遞(五)

新手寫(xiě)作虹脯,如果有錯(cuò)誤或者不嚴(yán)謹(jǐn)?shù)牡胤剑?qǐng)大伙給予指正奏候。如果這片文章對(duì)你有所幫助或者有所啟發(fā)循集,還請(qǐng)給一個(gè)贊,鼓勵(lì)一下作者蔗草,在此謝過(guò)咒彤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末疆柔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子镶柱,更是在濱河造成了極大的恐慌婆硬,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奸例,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡向楼,警方通過(guò)查閱死者的電腦和手機(jī)查吊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)湖蜕,“玉大人逻卖,你說(shuō)我怎么就攤上這事≌咽悖” “怎么了评也?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)灭返。 經(jīng)常有香客問(wèn)我盗迟,道長(zhǎng),這世上最難降的妖魔是什么熙含? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任罚缕,我火速辦了婚禮,結(jié)果婚禮上怎静,老公的妹妹穿的比我還像新娘邮弹。我一直安慰自己,他們只是感情好蚓聘,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布腌乡。 她就那樣靜靜地躺著,像睡著了一般夜牡。 火紅的嫁衣襯著肌膚如雪与纽。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天氯材,我揣著相機(jī)與錄音渣锦,去河邊找鬼。 笑死氢哮,一個(gè)胖子當(dāng)著我的面吹牛袋毙,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播冗尤,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼听盖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼胀溺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起皆看,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤仓坞,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后腰吟,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體无埃,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年毛雇,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嫉称。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡灵疮,死狀恐怖织阅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情震捣,我是刑警寧澤荔棉,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站蒿赢,受9級(jí)特大地震影響润樱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜羡棵,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一祥国、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧晾腔,春花似錦舌稀、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至剔应,卻和暖如春睡腿,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背峻贮。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工席怪, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人纤控。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓挂捻,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親船万。 傳聞我的和親對(duì)象是個(gè)殘疾皇子刻撒,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345