前端面試手寫代碼——call、apply篓冲、bind

1 call宠哄、apply毛嫉、bind 用法及對(duì)比

1.1 Function.prototype

三者都是Function原型上的方法承粤,所有函數(shù)都能調(diào)用它們

Function.prototype.call
Function.prototype.apply
Function.prototype.bind

1.2 語(yǔ)法

fn代表一個(gè)函數(shù)

fn.call(thisArg, arg1, arg2, ...) // 接收參數(shù)列表
fn.apply(thisArg, argsArray) // apply 接收數(shù)組參數(shù)
fn.bind(thisArg, arg1, arg2, ...) // 接收參數(shù)列表

1.3 參數(shù)說明

thisArg:在 fn 運(yùn)行時(shí)使用的 this 值

arg1,arg2,...:參數(shù)列表颜启,傳給 fn 使用的

argsArray:數(shù)組或類數(shù)組對(duì)象(比如Arguments對(duì)象)浪讳,傳給 fn 使用的

1.4 返回值

callapply:同 fn 執(zhí)行后的返回值

bind:返回一個(gè)原函數(shù)的拷貝负溪,并擁有指定的 this 值和初始參數(shù)川抡。并且返回的函數(shù)可以傳參崖堤。

const f = fn.bind(obj, arg1, arg2, ...)
f(a, b, c, ...)
// 調(diào)用 f 相當(dāng)于調(diào)用 fn.call(obj, ...args)
// args是調(diào)用bind傳入的參數(shù)加上調(diào)用f傳入的參數(shù)列表
// 即arg1,arg2...a,b,c...

1.5 作用

三個(gè)方法的作用相同:改變函數(shù)運(yùn)行時(shí)的this值密幔,可以實(shí)現(xiàn)函數(shù)的重用

1.6 用法舉例

function fn(a, b) {
    console.log(this.myName);
}

const obj = {
    myName: '蜜瓜'
}

fn(1, 2) 
// 輸出:undefined 
// 因?yàn)榇藭r(shí)this指向全局對(duì)象胯甩,全局對(duì)象上沒有myName屬性

fn.call(obj, 1, 2) 
fn.apply(obj, [1, 2])
// 輸出:蜜瓜
// 此時(shí)this指向obj偎箫,所以可以讀取到myName屬性

const fn1 = fn.bind(obj, 1, 2)
fn1()
// 輸出:蜜瓜
// 此時(shí)this指向obj淹办,所以可以讀取到myName屬性

1.7 三個(gè)方法的對(duì)比

方法 功能 參數(shù) 是否立即執(zhí)行
apply 改變函數(shù)運(yùn)行時(shí)的this 數(shù)組
call 改變函數(shù)運(yùn)行時(shí)的this 參數(shù)列表
bind 改變函數(shù)運(yùn)行時(shí)的this 參數(shù)列表 否娇唯。返回一個(gè)函數(shù)
  1. applycall會(huì)立即獲得執(zhí)行結(jié)果,而bind會(huì)返回一個(gè)已經(jīng)指定this和參數(shù)的函數(shù)拓哟,需要手動(dòng)調(diào)用此函數(shù)才會(huì)獲得執(zhí)行結(jié)果
  2. applycall唯一的區(qū)別就是參數(shù)形式不同
  3. 只有apply的參數(shù)是數(shù)組断序,記憶方法:apply和數(shù)組array都是a開頭

2 實(shí)現(xiàn)call违诗、apply诸迟、bind

2.1 實(shí)現(xiàn)call

2.1.1 易混淆的變量指向

現(xiàn)在我們來(lái)實(shí)現(xiàn)call方法壁公,命名為myCall

我們把它掛載到Function的原型上绅项,讓所有函數(shù)能調(diào)用這個(gè)方法

// 我們用剩余參數(shù)來(lái)接收參數(shù)列表
Function.prototype.myCall = function (thisArg, ...args) {
  console.log(this)
  console.log(thisArg)
}

首先要明白的是這個(gè)函數(shù)中this囊陡、thisArg分別指向什么

看看我們是怎么調(diào)用的:

fn.myCall(obj, arg1, arg2, ...)

所以关斜,myCall中的this指向fn痢畜,thisArg指向obj(目標(biāo)對(duì)象)

我們的目的是讓fn運(yùn)行時(shí)的this(注意這個(gè)thisfn中的)指向thisArg目標(biāo)對(duì)象

換句話說就是fn成為obj這個(gè)對(duì)象的方法來(lái)運(yùn)行(核心思路)

2.1.2 簡(jiǎn)易版call

我們根據(jù)上述核心思路可以寫出一個(gè)簡(jiǎn)單版本的myCall

Function.prototype.myCall = function (thisArg, ...args) {
  // 給thisArg新增一個(gè)方法
  thisArg.f = this; // this就是fn
  // 運(yùn)行這個(gè)方法鳍侣,傳入剩余參數(shù)
  let result = thisArg.f(...args);
  // 因?yàn)閏all方法的返回值同fn
  return result;
};

call方法的基本功能就完成了线衫,但是顯然存在問題:

  1. 倘若有多個(gè)函數(shù)同時(shí)調(diào)用這個(gè)方法惑折,并且目標(biāo)對(duì)象相同白热,則存在目標(biāo)對(duì)象的f屬性被覆蓋的可能
fn1.myCall(obj)
fn2.myCall(obj)
  1. 目標(biāo)對(duì)象上會(huì)永遠(yuǎn)存在這個(gè)屬性f

解決方案:

  1. ES6引入了一種新的原始數(shù)據(jù)類型Symbol屋确,表示獨(dú)一無(wú)二的值攻臀,最大的用法是用來(lái)定義對(duì)象的唯一屬性名刨啸。
  2. delete 操作符用于刪除對(duì)象的某個(gè)屬性

2.1.3 優(yōu)化明顯問題后的call

優(yōu)化后的myCall

Function.prototype.myCall = function (thisArg, ...args) {
  // 生成唯一屬性名设联,解決覆蓋的問題
  const prop = Symbol()
  // 注意這里不能用.
  thisArg[prop] = this; 
  // 運(yùn)行這個(gè)方法仑荐,傳入剩余參數(shù)粘招,同樣不能用.
  let result = thisArg[prop](...args);
  // 運(yùn)行完刪除屬性
  delete thisArg[prop]
  // 因?yàn)閏all方法的返回值同fn
  return result;
};

至此myCall方法的功能就相對(duì)完整了辑甜,但是還有一些細(xì)節(jié)需要補(bǔ)充

2.1.4 補(bǔ)充細(xì)節(jié)后的call

如果我們傳入的thisArg(目標(biāo)對(duì)象)是undefined或者null磷醋,我們就將其替換為指向全局對(duì)象(MDN文檔就是這么描述的)

// 完整代碼
Function.prototype.myCall = function (thisArg, ...args) {
  // 替換為全局對(duì)象:global或window
  thisArg = thisArg || global
  const prop = Symbol();
  thisArg[prop] = this;
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.2 實(shí)現(xiàn)apply

applycall實(shí)現(xiàn)思路一樣邓线,只是傳參形式不同

// 把剩余參數(shù)改成接收一個(gè)數(shù)組
Function.prototype.myApply = function (thisArg, args) {
  thisArg = thisArg || global
  // 判斷是否接收參數(shù)骇陈,若未接收參數(shù)你雌,替換為[]
  args = args || []
  const prop = Symbol();
  thisArg[prop] = this;
  // 用...運(yùn)算符展開傳入
  let result = thisArg[prop](...args);
  delete thisArg[prop];
  return result;
};

2.3 實(shí)現(xiàn)bind

2.3.1 簡(jiǎn)易版bind

實(shí)現(xiàn)思路:bind會(huì)創(chuàng)建一個(gè)新的綁定函數(shù)婿崭,它包裝了原函數(shù)對(duì)象氓栈,調(diào)用綁定函數(shù)會(huì)執(zhí)行被包裝的函數(shù)

前面已經(jīng)實(shí)現(xiàn)了callapply幸海,我們可以選用其中一個(gè)來(lái)綁定this,然后再封裝一層函數(shù)袜硫,就能得到一個(gè)簡(jiǎn)易版的方法:

Function.prototype.myBind = function(thisArg, ...args) {
  // this指向的是fn
  const self = this
  // 返回綁定函數(shù)
  return function() {
    // 包裝了原函數(shù)對(duì)象
    return self.apply(thisArg, args)
  }
}

2.3.2 注意點(diǎn)

  1. 注意apply的參數(shù)形式是數(shù)組婉陷,所以我們傳入的是args而非...args

  2. 為什么要在return前定義self來(lái)保存this闯睹?

    因?yàn)槲覀冃枰瞄]包將this(即fn)保存起來(lái)楼吃,使得myBind方法返回的函數(shù)在運(yùn)行時(shí)的this值能夠正確地指向fn

    具體解釋如下:

// 如果不定義self
Function.prototype.myBind = function(thisArg, ...args) {
  return function() {
    return this.apply(thisArg, args)
  }
}
const f = fn.myBind(obj) // 返回一個(gè)函數(shù)
// 為了看得清楚,寫成下面這種形式
// 其中thisArg躬窜、args保存在內(nèi)存中荣挨,這是因?yàn)樾纬闪碎]包
const f = function() {
  return this.apply(thisArg, args)
}
// 現(xiàn)在我們調(diào)用f
// 會(huì)發(fā)現(xiàn)其this指向全局對(duì)象(window/global)
// 而非我們期望的fn
f()

2.3.3 讓bind返回的函數(shù)(綁定函數(shù))可以傳參

前面說了bind返回的參數(shù)可以傳參(見1.4)垦沉,現(xiàn)在來(lái)對(duì)myBind進(jìn)行改進(jìn):

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  // 返回綁定函數(shù),用剩余參數(shù)接收參數(shù)
  return function(...innerArgs) {
    // 合并兩次傳入的參數(shù)
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}

2.3.4 “new + 綁定函數(shù)”存在什么問題

MDN:綁定函數(shù)也可以使用 new 運(yùn)算符構(gòu)造仍劈,它會(huì)表現(xiàn)為目標(biāo)函數(shù)已經(jīng)被構(gòu)建完畢了似的厕倍。提供的 this 值會(huì)被忽略,但前置參數(shù)仍會(huì)提供給模擬函數(shù)贩疙。

這是MDN文檔中的描述讹弯,意思是綁定函數(shù)可以作為構(gòu)造函數(shù)來(lái)創(chuàng)建實(shí)例,并且先前作為bind方法的第一個(gè)參數(shù)傳入的目標(biāo)對(duì)象thisArg失效这溅,但是先前提供的參數(shù)依然有效组民。

先來(lái)看我們的myBind

綁定函數(shù)的內(nèi)部:

// 綁定函數(shù)f
function(...innerArgs) {
  ...
  // 為了看得清楚,這里直接將self寫成了fn
  return fn.apply(thisArg, finalArgs)
}

new來(lái)創(chuàng)建f的實(shí)例:

const o = new f()

我們都知道(如果不知道看這篇:手寫實(shí)現(xiàn)new)悲靴,new的過程中會(huì)執(zhí)行構(gòu)造函數(shù)的代碼癞尚,即此處綁定函數(shù)f中的代碼會(huì)被執(zhí)行。

包括fn.apply(thisArg, finalArgs)這句代碼爽彤,并且其中的thisArg仍然有效匙瘪,這就不符合原生bind方法的描述了

2.3.5 綁定函數(shù)中怎么區(qū)分是否使用了new

如何解決:用new創(chuàng)建綁定函數(shù)的實(shí)例時(shí)碍论,讓先前傳入的thisArg失效

事實(shí)上對(duì)于綁定函數(shù)f來(lái)說藏研,執(zhí)行時(shí)的this值并不確定凳忙。

  1. 如果我們直接執(zhí)行f柳恐,那么綁定函數(shù)中的this指向全局對(duì)象巫俺。

  2. 如果我們用new來(lái)創(chuàng)建f的實(shí)例舶沛,那么f中的this指向新創(chuàng)建的實(shí)例。(這點(diǎn)如果不清楚看這篇:手寫實(shí)現(xiàn)new

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  return function(...innerArgs) {
    console.log(this) // 注意此處的this并不確定
    const finalArgs = [...args, ...innerArgs]
    return self.apply(thisArg, finalArgs)
  }
}
// 綁定函數(shù)
const f = fn.myBind(obj)
// 如果我們直接執(zhí)行f往毡,那么綁定函數(shù)中的this指向全局對(duì)象
f()
// 如果我們用new來(lái)創(chuàng)建f的實(shí)例个扰,那么f中的this指向新創(chuàng)建的實(shí)例
const o = new f()

基于上述兩種情況恐锣,我們可以修改myBind返回的綁定函數(shù),在函數(shù)內(nèi)對(duì)this值進(jìn)行判斷,從而區(qū)分是否使用了new運(yùn)算符

對(duì)myBind進(jìn)行如下更改:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // 以此來(lái)判斷是否使用了new
    if (isNew) {
      
    } 
    // 未使用new就跟原來(lái)一樣返回
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

2.3.6 補(bǔ)充完綁定函數(shù)內(nèi)部操作

現(xiàn)在我們需要知道如果是new構(gòu)造實(shí)例的情況應(yīng)該進(jìn)行哪些操作额获。

看看使用原生bind方法是什么結(jié)果:

const fn = function(a, b) {
  this.a = a
  this.b = b
}
const targetObj = {
  name: '蜜瓜'
}
// 綁定函數(shù)
const bound = fn.bind(targetObj, 1)
const o = new bound(2)
console.log(o); // fn { a: 1, b: 2 }
console.log(o.constructor); // [Function: fn]
console.log(o.__proto__ === fn.prototype); // true

可以看到剔难,new bound()返回的是以fn為構(gòu)造函數(shù)創(chuàng)建的實(shí)例。

根據(jù)這點(diǎn)可以補(bǔ)充完if (new) {}中的代碼:

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound // 以此來(lái)判斷是否使用了new
    if (isNew) {
      // 直接創(chuàng)建fn的實(shí)例
      return new self(...finalArgs)
    } 
    // 未使用new就跟原來(lái)一樣返回
    return self.apply(thisArg, finalArgs)
  }
  return bound
}
const bound = fn.myBind(targetObj, 1)
const o = new bound(2)

這樣桦锄,const o = new bound(2)相當(dāng)于const o = new self(...finalArgs)黑毅,因?yàn)闃?gòu)造函數(shù)如果顯式返回一個(gè)對(duì)象愿卒,就會(huì)直接覆蓋new過程中創(chuàng)建的對(duì)象(不知道的話可以看看這篇:手寫實(shí)現(xiàn)new

2.3.7 完整代碼

Function.prototype.myBind = function(thisArg, ...args) {
  const self = this
  const bound = function(...innerArgs) {
    const finalArgs = [...args, ...innerArgs]
    const isNew = this instanceof bound
    if (isNew) {
      return new self(...finalArgs)
    } 
    return self.apply(thisArg, finalArgs)
  }
  return bound
}

事實(shí)上飞主,這段代碼仍存在和原生bind出入的地方魁瞪,但是這里只是表達(dá)實(shí)現(xiàn)bind的一個(gè)整體思路旅薄,不必苛求完全一致

3 補(bǔ)充

  1. apply第焰、call方法還有一些細(xì)節(jié)我們沒有實(shí)現(xiàn):如果這個(gè)函數(shù)(fn)處于非嚴(yán)格模式下挺举,則指定為 nullundefined 時(shí)會(huì)自動(dòng)替換為指向全局對(duì)象瞻佛,原始值會(huì)被包裝(比如1會(huì)被包裝類Number包裝成對(duì)象)适刀。
  2. bind方法也是函數(shù)柯里化的一個(gè)應(yīng)用折欠,不熟悉柯里化的可以看看這篇內(nèi)容:JS函數(shù)柯里化

公眾號(hào)【前端嘛】獲取更多前端優(yōu)質(zhì)內(nèi)容

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末斤葱,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子揖闸,更是在濱河造成了極大的恐慌揍堕,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評(píng)論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件汤纸,死亡現(xiàn)場(chǎng)離奇詭異衩茸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)贮泞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門楞慈,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人啃擦,你說我怎么就攤上這事囊蓝。” “怎么了令蛉?”我有些...
    開封第一講書人閱讀 169,589評(píng)論 0 363
  • 文/不壞的土叔 我叫張陵聚霜,是天一觀的道長(zhǎng)狡恬。 經(jīng)常有香客問我,道長(zhǎng)蝎宇,這世上最難降的妖魔是什么弟劲? 我笑而不...
    開封第一講書人閱讀 60,188評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮姥芥,結(jié)果婚禮上兔乞,老公的妹妹穿的比我還像新娘。我一直安慰自己凉唐,他們只是感情好庸追,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,185評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著熊榛,像睡著了一般锚国。 火紅的嫁衣襯著肌膚如雪腕巡。 梳的紋絲不亂的頭發(fā)上玄坦,一...
    開封第一講書人閱讀 52,785評(píng)論 1 314
  • 那天,我揣著相機(jī)與錄音绘沉,去河邊找鬼煎楣。 笑死,一個(gè)胖子當(dāng)著我的面吹牛车伞,可吹牛的內(nèi)容都是我干的择懂。 我是一名探鬼主播,決...
    沈念sama閱讀 41,220評(píng)論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼另玖,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼困曙!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起谦去,我...
    開封第一講書人閱讀 40,167評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤慷丽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后鳄哭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體要糊,經(jīng)...
    沈念sama閱讀 46,698評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,767評(píng)論 3 343
  • 正文 我和宋清朗相戀三年妆丘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了锄俄。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,912評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡勺拣,死狀恐怖奶赠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情药有,我是刑警寧澤毅戈,帶...
    沈念sama閱讀 36,572評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響竹祷,放射性物質(zhì)發(fā)生泄漏谈跛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,254評(píng)論 3 336
  • 文/蒙蒙 一塑陵、第九天 我趴在偏房一處隱蔽的房頂上張望感憾。 院中可真熱鬧,春花似錦令花、人聲如沸阻桅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嫂沉。三九已至,卻和暖如春扮碧,著一層夾襖步出監(jiān)牢的瞬間趟章,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工慎王, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蚓土,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,359評(píng)論 3 379
  • 正文 我出身青樓赖淤,卻偏偏與公主長(zhǎng)得像蜀漆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子咱旱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,922評(píng)論 2 361

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