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 返回值
call
、apply
:同 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ù) |
-
apply
和call
會(huì)立即獲得執(zhí)行結(jié)果,而bind
會(huì)返回一個(gè)已經(jīng)指定this
和參數(shù)的函數(shù)拓哟,需要手動(dòng)調(diào)用此函數(shù)才會(huì)獲得執(zhí)行結(jié)果 -
apply
和call
唯一的區(qū)別就是參數(shù)形式不同 - 只有
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è)this
是fn
中的)指向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
方法的基本功能就完成了线衫,但是顯然存在問題:
- 倘若有多個(gè)函數(shù)同時(shí)調(diào)用這個(gè)方法惑折,并且目標(biāo)對(duì)象相同白热,則存在目標(biāo)對(duì)象的
f
屬性被覆蓋的可能
fn1.myCall(obj)
fn2.myCall(obj)
- 目標(biāo)對(duì)象上會(huì)永遠(yuǎn)存在這個(gè)屬性
f
解決方案:
-
ES6
引入了一種新的原始數(shù)據(jù)類型Symbol
屋确,表示獨(dú)一無(wú)二的值攻臀,最大的用法是用來(lái)定義對(duì)象的唯一屬性名刨啸。 -
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
apply
和call
實(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)了call
和apply
幸海,我們可以選用其中一個(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)
注意
apply
的參數(shù)形式是數(shù)組婉陷,所以我們傳入的是args
而非...args
-
為什么要在
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
值并不確定凳忙。
如果我們直接執(zhí)行
f
柳恐,那么綁定函數(shù)中的this
指向全局對(duì)象巫俺。如果我們用
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ǔ)充
-
apply
第焰、call
方法還有一些細(xì)節(jié)我們沒有實(shí)現(xiàn):如果這個(gè)函數(shù)(fn)處于非嚴(yán)格模式下挺举,則指定為null
或undefined
時(shí)會(huì)自動(dòng)替換為指向全局對(duì)象瞻佛,原始值會(huì)被包裝(比如1
會(huì)被包裝類Number
包裝成對(duì)象)适刀。 -
bind
方法也是函數(shù)柯里化的一個(gè)應(yīng)用折欠,不熟悉柯里化的可以看看這篇內(nèi)容:JS函數(shù)柯里化
公眾號(hào)【前端嘛】獲取更多前端優(yōu)質(zhì)內(nèi)容