前言: 本來只是想寫一下簡(jiǎn)單的 bind 函數(shù)實(shí)現(xiàn),沒想到寫著寫著還能牽出 js 中繼承的知識(shí)恐锦,其實(shí)研究原生函數(shù)的實(shí)現(xiàn)總是能學(xué)到很多新東西
在實(shí)現(xiàn)之前呢,我們首先要知道bind是做什么的。JS MDN 給出的定義是The bind() methods creates a new function that, when called, has its this keyword set to the provided value, with a given sequence of arguments preceding any provided when the new function is called.,簡(jiǎn)單來說就是bind()函數(shù)創(chuàng)建了一個(gè)新函數(shù)(原函數(shù)的拷貝)挠进,這個(gè)函數(shù)接受一個(gè)提供新的this上下文的參數(shù)色乾,以及之后任意可選的其他參數(shù)。當(dāng)這個(gè)新函數(shù)被調(diào)用時(shí)领突,它的this關(guān)鍵字指向第一個(gè)參數(shù)的新上下文暖璧。而第二個(gè)之后的參數(shù)會(huì)與原函數(shù)的參數(shù)組成新參數(shù)(原函數(shù)的參數(shù)在后),傳遞給函數(shù)攘须。
弄清楚這個(gè)之后,我們?cè)賮矸治隹纯匆趺醋觥?/p>
首先殴泰,調(diào)用 bind() 會(huì)返回一個(gè)閉包于宙,這個(gè)閉包中創(chuàng)建了一個(gè)新函數(shù),這個(gè)函數(shù)首先包含原函數(shù)的屬性與方法悍汛,并且這個(gè)函數(shù)的 this 值是傳給 bind() 函數(shù)第一個(gè)參數(shù)捞魁,所以自然而然我們想到用 call 或者 apply 來改變?cè)瘮?shù)的 this ,這里我們選擇 apply离咐, 理由是我們新函數(shù)的第一個(gè)之后的參數(shù)是由傳給 bind() 的第二個(gè)及之后的參數(shù)(代碼中的 formerArgs )再加上原函數(shù)的參數(shù)(代碼中的 laterArgs )構(gòu)成的谱俭,我們把它們拼接成一個(gè)數(shù)組就完事兒了~具體代碼如下:
Function.prototype.bind = function (ctx) {
// 保存原函數(shù)的 this 至 _this
var _this = this
// slice 使用了兩次,保存到變量中
// slice 主要用于類數(shù)組對(duì)象 arguments 的淺復(fù)制
var slice = Array.prototype.slice
// 傳給 bind() 函數(shù)的第二個(gè)至之后的參數(shù)宵蛀,從? arguments 的第二位開始
var formerArgs = slice.call(arguments, 1)
// bind() 本身就是一個(gè)函數(shù)昆著,返回
return function (){
// 傳給原函數(shù)的參數(shù)
let laterArgs = slice.call(arguments, 0)
// 返回一個(gè)函數(shù),這個(gè)函數(shù)調(diào)用了原函數(shù)术陶,并且 this 指向 bind 的第一個(gè)參數(shù)凑懂,
// 第二個(gè)參數(shù)由 formerArgs? 與 laterArgs組成
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}
ES6 實(shí)現(xiàn)
上面的代碼是基于 ES5 實(shí)現(xiàn)的,當(dāng)時(shí)對(duì)于不確定的參數(shù)的一般處理方法都是利用類數(shù)組對(duì)象arguments(其中包含了傳遞給函數(shù)的所有參數(shù))梧宫,也就免不了使用call或者是apply對(duì)其進(jìn)行數(shù)組操作接谨。代碼也就顯得比較冗長(zhǎng)。但是 ES6 不一樣了呀~我們有了不定參數(shù)這個(gè)神器塘匣。無論有無參數(shù)脓豪,有幾個(gè)參數(shù)都可以簡(jiǎn)單地處理。
不定參數(shù): 傳遞給函數(shù)的最后一個(gè)參數(shù)可以被標(biāo)記為不定參數(shù)忌卤,當(dāng)函數(shù)被調(diào)用時(shí)扫夜,不定參數(shù)之前的參數(shù)都可正常被填充,剩下的參數(shù)會(huì)被放進(jìn)一個(gè)數(shù)組中驰徊,并被賦值給不定參數(shù)历谍。而當(dāng)沒有剩下的參數(shù)時(shí),不定參數(shù)會(huì)是一個(gè)空數(shù)組辣垒,而不會(huì)被填充為undefined望侈。
同樣的功能,只要幾行代碼就可以實(shí)現(xiàn):
// formerArgs 為傳遞給 bind 函數(shù)的第二個(gè)到之后的參數(shù)
Function.prototype.bind = function (ctx, ...formerArgs) {
let _this = this
// laterArgs 為傳遞給原函數(shù)的參數(shù)
return (...laterArgs) => {
// bind 函數(shù)的不定參數(shù)在原函數(shù)參數(shù)之前勋桶,formerArgs 本身就是數(shù)組脱衙,可以直接調(diào)用數(shù)組的 concat 方法侥猬,無需借助 call 或 apply
return _this.apply(ctx, formerArgs.concat(laterArgs))
}
}
至此,我們就實(shí)現(xiàn)了簡(jiǎn)單的 bind() 函數(shù)的功能捐韩,接下來我們給它做點(diǎn)優(yōu)化退唠。
優(yōu)化 UPUPUP..
·?當(dāng) Function 的原型鏈上沒有 bind 函數(shù)時(shí),才加上此函數(shù)
·if (!Function.prototype.bind) {
// add bind() to Function.prototype
}
·?只有函數(shù)才能調(diào)用 bind 函數(shù)荤胁,其他的對(duì)象不行瞧预。即判斷 this 是否為函數(shù)。
if (typeof this !== 'function') {
// throw NOT_A_FUNCTION error
}
·?壓軸戲: 關(guān)于繼承
我們上面的代碼使用了借用 apply 繼承的方式仅政。用了 apply 來改變 this 的指向垢油,繼承了原函數(shù)的基本屬性和引用屬性,并且保留了可傳參優(yōu)點(diǎn)圆丹,但是新函數(shù)無法實(shí)現(xiàn)函數(shù)復(fù)用滩愁,每個(gè)新函數(shù)都會(huì)復(fù)制出一份新的原函數(shù)的函數(shù),并且也無法繼承到原函數(shù)通過 prototype 方式定義的方法或?qū)傩浴?/p>
為解決以上問題辫封,我們選用組合繼承方式硝枉,在使用 apply 繼承的基礎(chǔ)上,加上了原型鏈繼承倦微。
所以我們可以這么改妻味。
if(!Function.prototype.binds) {
Function.prototype.binds =function(ctx){
if(typeofthis!=='function') {
thrownewTypeError("NOT_A_FUNCTION -- this is not callable")
}
var_this =this
varslice =Array.prototype.slice
varformerArgs = slice.call(arguments,1)
// 定義一個(gè)中間函數(shù),用于作為繼承的中間值
varfun =function(){}
varfBound =function(){
letlaterArgs = slice.call(arguments,0)
return_this.apply(ctx, formerArgs.concat(laterArgs))
}
// 先讓 fun 的原型方法指向 _this 即原函數(shù)的原型方法欣福,繼承 _this 的屬性
fun.prototype = _this.prototype
// 再將 fBound 即要返回的新函數(shù)的原型方法指向 fun 的實(shí)例化對(duì)象
// 這樣弧可,既能讓 fBound 繼承 _this 的屬性,在修改其原型鏈時(shí)劣欢,又不會(huì)影響到 _this 的原型鏈
fBound.prototype =newfun()
returnfBound
}
}
在上面的代碼中棕诵,我們引入了一個(gè)新的函數(shù) fun,用于繼承原函數(shù)的原型凿将,并通過 new 操作符實(shí)例化出它的實(shí)例對(duì)象校套,供 fBound 的原型繼承,至此牧抵,我們既讓新函數(shù)繼承了原函數(shù)的所有屬性與方法笛匙,又保證了不會(huì)因?yàn)槠鋵?duì)原型鏈的操作影響到原函數(shù)。用圖來表示應(yīng)該是下面這樣的:
這樣我們對(duì)新函數(shù)的 prototype 修改只會(huì)應(yīng)用在它自己身上犀变,而不會(huì)影響到原函數(shù)妹孙。
其他
MDN 中還提到,若是將 bind 綁定之后的函數(shù)當(dāng)作構(gòu)造函數(shù)获枝,通過 new 操作符使用蠢正,則不綁定傳入的 this姚淆,而是將 this 指向?qū)嵗鰜淼膶?duì)象去枷。所以我們最終的代碼為:
if (!Function.prototype.binds) {
Function.prototype.binds = function (ctx) {
if (typeof this !== 'function') {
throw new TypeError("NOT_A_FUNCTION -- this is not callable")
}
var _this = this
var slice = Array.prototype.slice
var formerArgs = slice.call(arguments, 1)
var fun = function () {}
var fBound = function (){
let laterArgs = slice.call(arguments, 0)
// 若通過 new 調(diào)用 bind() 之后的函數(shù)挑辆,則這時(shí)候 fBound 的 this 指向的是 fBound 實(shí)例宾尚,
// 而下面又定義了 fBound 是 fun 的派生類(其 prototype 指向 fun 的實(shí)例),
// 所以 this instanceof fun === true 雹舀,這時(shí) this 指向了 fBound 實(shí)例芦劣,不另外綁定!
return _this.apply(this instanceof fun ? this : ctx || this, formerArgs.concat(laterArgs))
}
fun.prototype = _this.prototype
fBound.prototype = new fun()
return fBound
}
}
打完收工~~歐耶 ( ?? ω ?? )y
P.S: ES7 中已經(jīng)淘汰了 .bind 的寫法说榆,而是使用兩個(gè)冒號(hào)的方式 :: 來代替虚吟,(要不要這么可愛!不過呢签财,人家還只是個(gè)提案而已串慰,到時(shí)候會(huì)不會(huì)被采用還不一定的呢
用法有兩種,第一種::對(duì)象.方法名
letobj = {
val:'value',
method:function(){
console.log(this.val)
}
};
// ::對(duì)象.方法名
// 等價(jià)于 obj.method.bind(obj)
// 將 method 的 this 綁定為 obj
// 這里的 method 必須是 obj 的方法
::obj.method;
// call
obj.method();// value
第二種荠卷,對(duì)象::方法名()
letobj = {
val:'value'
};
functionmethod(){
console.log(this.val);
}
// 對(duì)象::方法名()
// 等價(jià)于 method.call(obj) 或 method.apply(obj)
// 會(huì)直接調(diào)用并輸出 'value'
// obj::method();
// 對(duì)象::方法? (無括號(hào)
// 等價(jià)于 method.bind(obj)
// 不會(huì)自動(dòng)執(zhí)行模庐,必須賦值給 method 才能實(shí)現(xiàn)綁定
// 手動(dòng)調(diào)用 method 烛愧,輸出 vaue
method = obj::method;
method();
嘛~ 有說的不對(duì)的地方油宜,歡迎指正..
文章來源:https://jothy1023.github.io/2016/10/16/js-bind-es5-es6/
作者:四月(轉(zhuǎn)載已經(jīng)獲得作者授權(quán))