面試季,這些函數(shù)知識(shí)總結(jié)請(qǐng)收下


這段時(shí)間我試著通過(guò)思維導(dǎo)圖來(lái)總結(jié)知識(shí)點(diǎn),主要關(guān)注的是一些相對(duì)重要或理解難度較高的內(nèi)容。下面是同系列文章:
「思維導(dǎo)圖學(xué)前端 」6k字一文搞懂Javascript對(duì)象,原型,繼承[1] 「思維導(dǎo)圖學(xué)前端 」初中級(jí)前端值得收藏的正則表達(dá)式知識(shí)點(diǎn)掃盲[2]
如果您需要換個(gè)角度看閉包,請(qǐng)直接打開(kāi)解讀閉包,這次從ECMAScript詞法環(huán)境,執(zhí)行上下文說(shuō)起[3]。
本文總結(jié)了javascript中函數(shù)的常見(jiàn)知識(shí)點(diǎn),包含了基礎(chǔ)概念,閉包,this指向問(wèn)題,高階函數(shù),柯里化等,手寫(xiě)代碼那部分也是滿滿的干貨,無(wú)論您是想復(fù)習(xí)準(zhǔn)備面試,還是想深入了解原理,本文都應(yīng)該有你想看的點(diǎn),總之還是值得一看的。
老規(guī)矩,先上思維導(dǎo)圖。

什么是函數(shù)
一般來(lái)說(shuō),一個(gè)函數(shù)是可以通過(guò)外部代碼調(diào)用的一個(gè)“子程序”(或在遞歸的情況下由內(nèi)部函數(shù)調(diào)用)。像程序本身一樣,一個(gè)函數(shù)由稱為函數(shù)體的一系列語(yǔ)句組成。值可以傳遞給一個(gè)函數(shù),函數(shù)將返回一個(gè)值。
函數(shù)首先是一個(gè)對(duì)象,并且在javascript中,函數(shù)是一等對(duì)象(first-class object)。函數(shù)可以被執(zhí)行(callable,擁有內(nèi)部屬性[[Call]]),這是函數(shù)的本質(zhì)特性。除此之外,函數(shù)可以賦值給變量,也可以作為函數(shù)參數(shù),還可以作為另一個(gè)函數(shù)的返回值。
函數(shù)基本概念
函數(shù)名
函數(shù)名是函數(shù)的標(biāo)識(shí),如果一個(gè)函數(shù)不是匿名函數(shù),它應(yīng)該被賦予函數(shù)名。
函數(shù)命名需要符合javascript標(biāo)識(shí)符規(guī)則,必須以字母、下劃線_或美元符 $ 開(kāi)始,后面可以跟數(shù)字,字母,下劃線,美元符。 函數(shù)命名不能使用javascript保留字,保留字是javascript中具有特殊含義的標(biāo)識(shí)符。 函數(shù)命名應(yīng)該語(yǔ)義化,盡量采用動(dòng)賓結(jié)構(gòu),小駝峰寫(xiě)法,比如 getUserName(),validateForm(),isValidMobilePhone()。對(duì)于構(gòu)造函數(shù),我們通常寫(xiě)成大駝峰格式(因?yàn)闃?gòu)造函數(shù)與類(lèi)的概念強(qiáng)關(guān)聯(lián))。
下面是一些不成文的約定,不成文代表它不必遵守,但是我們按照這樣的約定來(lái)執(zhí)行,會(huì)讓開(kāi)發(fā)變得更有效率。
__xxx__代表非標(biāo)準(zhǔn)的方法。_xxx代表私有方法。
函數(shù)參數(shù)
形參
形參是函數(shù)定義時(shí)約定的參數(shù)列表,由一對(duì)圓括號(hào)()包裹。
在MDN上有看到,一個(gè)函數(shù)最多可以有255個(gè)參數(shù)。
然而形參太多時(shí),使用者總是容易在引用時(shí)出錯(cuò)。所以對(duì)于數(shù)量較多的形參,一般推薦把所有參數(shù)作為屬性或方法整合到一個(gè)對(duì)象中,各個(gè)參數(shù)作為這個(gè)對(duì)象的屬性或方法來(lái)使用。舉個(gè)例子,微信小程序的提供的API基本上是這種調(diào)用形式。
wx.redirectTo(Object object)
調(diào)用示例如下:
wx.redirectTo({
url: '/article/detail?id=1',
success: function() {},
fail: function() {}
})
形參的數(shù)量可以由函數(shù)的length屬性獲得,如下所示。
function test(a, b, c) {}
test.length; // 3
實(shí)參
實(shí)參是調(diào)用函數(shù)時(shí)傳入的,實(shí)參的值在函數(shù)執(zhí)行前被確定。
javascript在函數(shù)定義時(shí)并不會(huì)約定參數(shù)的數(shù)據(jù)類(lèi)型。如果你期望函數(shù)調(diào)用時(shí)傳入正確的數(shù)據(jù)類(lèi)型,你必須在函數(shù)體中對(duì)入?yún)⑦M(jìn)行數(shù)據(jù)類(lèi)型判斷。
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error("參數(shù)必須是數(shù)字類(lèi)型")
}
}
好在Typescript提供了數(shù)據(jù)類(lèi)型檢查的能力,這一定程度上防止了意外情況的發(fā)生。
實(shí)參的數(shù)量可以通過(guò)函數(shù)中arguments對(duì)象的length屬性獲得,如下所示。
實(shí)參數(shù)量不一定與形參數(shù)量一致。
function test(a, b, c) {
var argLength = arguments.length;
return argLength;
}
test(1, 2); // 2
默認(rèn)參數(shù)
函數(shù)參數(shù)的默認(rèn)值是undefined,如果你不傳入實(shí)參,那么實(shí)際上在函數(shù)執(zhí)行過(guò)程中,相應(yīng)參數(shù)的值是undefined。
ES6也支持在函數(shù)聲明時(shí)設(shè)置參數(shù)的默認(rèn)值。
function add(a, b = 2) {
return a + b;
}
add(1); // 3
在上面的add函數(shù)中,參數(shù)b被指定了默認(rèn)值2。所以,即便你不傳第二個(gè)參數(shù)b,也能得到一個(gè)預(yù)期的結(jié)果。
假設(shè)一個(gè)函數(shù)有多個(gè)參數(shù),我們希望不給中間的某個(gè)參數(shù)傳值,那么這個(gè)參數(shù)值必須顯示地指定為undefined,否則我們期望傳給后面的參數(shù)的值會(huì)被傳到中間的這個(gè)參數(shù)。
function printUserInfo(name, age = 18, gender) {
console.log(`姓名:${name},年齡:${age},性別:${gender}`);
}
// 正確地使用
printUserInfo('Bob', undefined, 'male');
// 錯(cuò)誤,'male'被錯(cuò)誤地傳給了age參數(shù)
printUserInfo('Bob', 'male');
PS:注意,如果你希望使用參數(shù)的默認(rèn)值,請(qǐng)一定傳undefined,而不是null。
當(dāng)然,我們也可以在函數(shù)體中判斷參數(shù)的數(shù)據(jù)類(lèi)型,防止參數(shù)被誤用。
function printUserInfo(name, age = 18, gender) {
if (typeof arguments[1] === 'string') {
age = 18;
gender = arguments[1];
}
console.log(`姓名:${name},年齡:${age},性別:${gender}`);
}
printUserInfo('bob', 'male'); // 姓名:bob,年齡:18,性別:male
這樣一來(lái),函數(shù)的邏輯也不會(huì)亂。
剩余參數(shù)
剩余參數(shù)語(yǔ)法允許我們將一個(gè)不定數(shù)量的參數(shù)表示為一個(gè)數(shù)組。
剩余參數(shù)通過(guò)剩余語(yǔ)法...將多個(gè)參數(shù)聚合成一個(gè)數(shù)組。
function add(a, ...args) {
return args.reduce((prev, curr) => {
return prev + curr
}, a)
}
剩余參數(shù)和arguments對(duì)象之間的區(qū)別主要有三個(gè):
剩余參數(shù)只包含那些沒(méi)有對(duì)應(yīng)形參的實(shí)參,而 arguments對(duì)象包含了傳給函數(shù)的所有實(shí)參。arguments對(duì)象不是一個(gè)真正的數(shù)組,而剩余參數(shù)是真正的Array實(shí)例,也就是說(shuō)你能夠在它上面直接使用所有的數(shù)組方法,比如sort,map,forEach或pop。而arguments需要借用call來(lái)實(shí)現(xiàn),比如[].slice.call(arguments)。arguments對(duì)象還有一些附加的屬性(如callee屬性)。
剩余語(yǔ)法和展開(kāi)運(yùn)算符看起來(lái)很相似,然而從功能上來(lái)說(shuō),是完全相反的。
剩余語(yǔ)法(Rest syntax) 看起來(lái)和展開(kāi)語(yǔ)法完全相同,不同點(diǎn)在于, 剩余參數(shù)用于解構(gòu)數(shù)組和對(duì)象。從某種意義上說(shuō),剩余語(yǔ)法與展開(kāi)語(yǔ)法是相反的:展開(kāi)語(yǔ)法將數(shù)組展開(kāi)為其中的各個(gè)元素,而剩余語(yǔ)法則是將多個(gè)元素收集起來(lái)并“凝聚”為單個(gè)元素。
arguments
函數(shù)的實(shí)際參數(shù)會(huì)被保存在一個(gè)類(lèi)數(shù)組對(duì)象arguments中。
類(lèi)數(shù)組(ArrayLike)對(duì)象具備一個(gè)非負(fù)的length屬性,并且可以通過(guò)從0開(kāi)始的索引去訪問(wèn)元素,讓人看起來(lái)覺(jué)得就像是數(shù)組,比如NodeList,但是類(lèi)數(shù)組默認(rèn)沒(méi)有數(shù)組的那些內(nèi)置方法,比如push, pop, forEach, map。
我們可以試試,隨便找一個(gè)網(wǎng)站,在控制臺(tái)輸入:
var linkList = document.querySelectorAll('a')
會(huì)得到一個(gè)NodeList,我們也可以通過(guò)數(shù)字下標(biāo)去訪問(wèn)其中的元素,比如linkList[0]。
但是NodeList不是數(shù)組,它是類(lèi)數(shù)組。
Array.isArray(linkList); // false
回到主題,arguments也是類(lèi)數(shù)組,arguments的length由實(shí)參的數(shù)量決定,而不是由形參的數(shù)量決定。
function add(a, b) {
console.log(arguments.length);
return a + b;
}
add(1, 2, 3, 4);
// 這里打印的是4,而不是2
arguments也是一個(gè)和嚴(yán)格模式有關(guān)聯(lián)的對(duì)象。
在非嚴(yán)格模式下, arguments里的元素和函數(shù)參數(shù)都是指向同一個(gè)值的引用,對(duì)arguments的修改,會(huì)直接影響函數(shù)參數(shù)。
function test(obj) {
arguments[0] = '傳入的實(shí)參是一個(gè)對(duì)象,但是被我變成字符串了'
console.log(obj)
}
test({name: 'jack'})
// 這里打印的是字符串,而不是對(duì)象
在嚴(yán)格模式下, arguments是函數(shù)參數(shù)的副本,對(duì)arguments的修改不會(huì)影響函數(shù)參數(shù)。但是arguments不能重新被賦值,關(guān)于這一點(diǎn),我在解讀閉包,這次從ECMAScript詞法環(huán)境,執(zhí)行上下文說(shuō)起[3]這篇文章中解讀不可變綁定時(shí)有提到。在嚴(yán)格模式下,也不能使用arguments.caller和arguments.callee,限制了對(duì)調(diào)用棧的檢測(cè)能力。
函數(shù)體
函數(shù)體(FunctionBody)是函數(shù)的主體,其中的函數(shù)代碼(function code)由一對(duì)花括號(hào){}包裹。函數(shù)體可以為空,也可以由任意條javascript語(yǔ)句組成。
函數(shù)的調(diào)用形式
大體來(lái)說(shuō),函數(shù)的調(diào)用形式分為以下四種:
作為普通函數(shù)
函數(shù)作為普通函數(shù)被調(diào)用,這是函數(shù)調(diào)用的常用形式。
function add(a, b) {
return a + b;
}
add(); // 調(diào)用add函數(shù)
作為普通函數(shù)調(diào)用時(shí),如果在非嚴(yán)格模式下,函數(shù)執(zhí)行時(shí),this指向全局對(duì)象,對(duì)于瀏覽器而言則是window對(duì)象;如果在嚴(yán)格模式下,this的值則是undefined。
作為對(duì)象的方法
函數(shù)也可以作為對(duì)象的成員,這種情況下,該函數(shù)通常被稱為對(duì)象方法。當(dāng)函數(shù)作為對(duì)象的方法被調(diào)用時(shí),this指向該對(duì)象,此時(shí)便可以通過(guò)this訪問(wèn)對(duì)象的其他成員變量或方法。
var counter = {
num: 0,
increase: function() {
this.num++;
}
}
counter.increase();
作為構(gòu)造函數(shù)
函數(shù)配合new關(guān)鍵字使用時(shí)就成了構(gòu)造函數(shù)。構(gòu)造函數(shù)用于實(shí)例化對(duì)象,構(gòu)造函數(shù)的執(zhí)行過(guò)程大致如下:
首先創(chuàng)建一個(gè)新對(duì)象,這個(gè)新對(duì)象的 __proto__屬性指向構(gòu)造函數(shù)的prototype屬性。此時(shí)構(gòu)造函數(shù)的 this指向這個(gè)新對(duì)象。執(zhí)行構(gòu)造函數(shù)中的代碼,一般是通過(guò) this給新對(duì)象添加新的成員屬性或方法。最后返回這個(gè)新對(duì)象。
實(shí)例化對(duì)象也可以通過(guò)一些技巧來(lái)簡(jiǎn)化,比如在構(gòu)造函數(shù)中顯示地return另一個(gè)對(duì)象,jQuery很巧妙地利用了這一點(diǎn)。具體分析詳見(jiàn)面試官真的會(huì)問(wèn):new的實(shí)現(xiàn)以及無(wú)new實(shí)例化[4]。
通過(guò)call, apply調(diào)用
apply和call是函數(shù)對(duì)象的原型方法,掛載于Function.prototype。利用這兩個(gè)方法,我們可以顯示地綁定一個(gè)this作為調(diào)用上下文,同時(shí)也可以設(shè)置函數(shù)調(diào)用時(shí)的參數(shù)。
apply和call的區(qū)別在于:提供參數(shù)的形式不同,apply方法接受的是一個(gè)參數(shù)數(shù)組,call方法接受的是參數(shù)列表。
someFunc.call(obj, 1, 2, 3)
someFunc.apply(obj, [1, 2, 3])
注意,在非嚴(yán)格模式下使用call或者apply時(shí),如果第一個(gè)參數(shù)被指定為null或undefined,那么函數(shù)執(zhí)行時(shí)的this指向全局對(duì)象(瀏覽器環(huán)境中是window);如果第一個(gè)參數(shù)被指定為原始值,該原始值會(huì)被包裝。這部分內(nèi)容在下文中的手寫(xiě)代碼會(huì)再次講到。
call是用來(lái)實(shí)現(xiàn)繼承的重要方法。在子類(lèi)構(gòu)造函數(shù)中,通過(guò)call來(lái)調(diào)用父類(lèi)構(gòu)造函數(shù),以使對(duì)象實(shí)例獲得來(lái)自父類(lèi)構(gòu)造函數(shù)的屬性或方法。
function Father() {
this.nationality = 'Han';
};
Father.prototype.propA = '我是父類(lèi)原型上的屬性';
function Child() {
Father.call(this);
};
Child.prototype.propB = '我是子類(lèi)原型上的屬性';
var child = new Child();
child.nationality; // "Han"
call, apply, bind
call,apply,bind都可以綁定this,區(qū)別在于:apply和call是綁定this后直接調(diào)用該函數(shù),而bind會(huì)返回一個(gè)新的函數(shù),并不直接調(diào)用,可以由程序員決定調(diào)用的時(shí)機(jī)。
bind的語(yǔ)法形式如下:
function.bind(thisArg[, arg1[, arg2[, ...]]])
bind的arg1, arg2, ...是給新函數(shù)預(yù)置好的參數(shù)(預(yù)置參數(shù)是可選的)。當(dāng)然新函數(shù)在執(zhí)行時(shí)也可以繼續(xù)追加參數(shù)。
手寫(xiě)call, apply, bind
提到call,apply,bind總是無(wú)法避免手寫(xiě)代碼這個(gè)話題。手寫(xiě)代碼不僅僅是為了應(yīng)付面試,也是幫助我們理清思路和深入原理的一個(gè)好方法。手寫(xiě)代碼一定不要抄襲,如果實(shí)在沒(méi)思路,可以參考下別人的代碼整理出思路,再自己按照思路獨(dú)立寫(xiě)一遍代碼,然后驗(yàn)證看看有沒(méi)有缺陷,這樣才能有所收獲,否則忘得很快,只能短時(shí)間應(yīng)付應(yīng)付。
那么如何才能順利地手寫(xiě)代碼呢?首先是要清楚一段代碼的作用,可以從官方對(duì)于它的定義和描述入手,同時(shí)還要注意一些特殊情況下的處理。
就拿call來(lái)說(shuō),call是函數(shù)對(duì)象的原型方法,它的作用是綁定this和參數(shù),并執(zhí)行函數(shù)。調(diào)用形式如下:
function.call(thisArg, arg1, arg2, ...)
那么我們慢慢來(lái)實(shí)現(xiàn)它,將我們要實(shí)現(xiàn)的函數(shù)命名為myCall。首先myCall是一個(gè)函數(shù),接受的第一個(gè)參數(shù)thisArg是目標(biāo)函數(shù)執(zhí)行時(shí)的this的值,從第二個(gè)可選參數(shù)arg1開(kāi)始的其他參數(shù)將作為目標(biāo)函數(shù)執(zhí)行時(shí)的實(shí)參。
這里面有很多細(xì)節(jié)要考慮,我大致羅列了一下:
要考慮是不是嚴(yán)格模式。如果是非嚴(yán)格模式,對(duì)于 thisArg要特殊處理。如何判斷嚴(yán)格模式? thisArg被處理后還要進(jìn)行非空判斷,然后考慮是以方法的形式調(diào)用還是以普通函數(shù)的形式調(diào)用。目標(biāo)函數(shù)作為方法調(diào)用時(shí),如何不覆蓋對(duì)象的原有屬性?
實(shí)現(xiàn)代碼如下,請(qǐng)仔細(xì)看我寫(xiě)的注釋,這是主要的思路!
// 首先apply是Function.prototype上的一個(gè)方法
Function.prototype.myCall = function() {
// 由于目標(biāo)函數(shù)的實(shí)參數(shù)量是不定的,這里就不寫(xiě)形參了
// 實(shí)際上通過(guò)arugments對(duì)象,我們能拿到所有實(shí)參
// 第一個(gè)參數(shù)是綁定的this
var thisArg = arguments[0];
// 接著要判斷是不是嚴(yán)格模式
var isStrict = (function(){return this === undefined}())
if (!isStrict) {
// 如果在非嚴(yán)格模式下,thisArg的值是null或undefined,需要將thisArg置為全局對(duì)象
if (thisArg === null || thisArg === undefined) {
// 獲取全局對(duì)象時(shí)兼顧瀏覽器環(huán)境和Node環(huán)境
thisArg = (function(){return this}())
} else {
// 如果是其他原始值,需要通過(guò)構(gòu)造函數(shù)包裝成對(duì)象
var thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
}
// 截取從索引1開(kāi)始的剩余參數(shù)
var invokeParams = [...arguments].slice(1);
// 接下來(lái)要調(diào)用目標(biāo)函數(shù),那么如何獲取到目標(biāo)函數(shù)呢?
// 實(shí)際上this就是目標(biāo)函數(shù),因?yàn)閙yCall是作為一個(gè)方法被調(diào)用的,this當(dāng)然指向調(diào)用對(duì)象,而這個(gè)對(duì)象就是目標(biāo)函數(shù)
// 這里做這么一個(gè)賦值過(guò)程,是為了讓語(yǔ)義更清晰一點(diǎn)
var invokeFunc = this;
// 此時(shí)如果thisArg對(duì)象仍然是null或undefined,那么說(shuō)明是在嚴(yán)格模式下,并且沒(méi)有指定第一個(gè)參數(shù)或者第一個(gè)參數(shù)的值本身就是null或undefined,此時(shí)將目標(biāo)函數(shù)當(dāng)成普通函數(shù)執(zhí)行并返回其結(jié)果即可
if (thisArg === null || thisArg === undefined) {
return invokeFunc(...invokeParams)
}
// 否則,讓目標(biāo)函數(shù)成為thisArg對(duì)象的成員方法,然后調(diào)用它
// 直觀上來(lái)看,可以直接把目標(biāo)函數(shù)賦值給對(duì)象屬性,比如func屬性,但是可能func屬性本身就存在于thisArg對(duì)象上
// 所以,為了防止覆蓋掉thisArg對(duì)象的原有屬性,必須創(chuàng)建一個(gè)唯一的屬性名,可以用Symbol實(shí)現(xiàn),如果環(huán)境不支持Symbol,可以通過(guò)uuid算法來(lái)構(gòu)造一個(gè)唯一值。
var uniquePropName = Symbol(thisArg)
thisArg[uniquePropName] = invokeFunc
// 返回目標(biāo)函數(shù)執(zhí)行的結(jié)果
return thisArg[uniquePropName](...invokeParams "uniquePropName")
}
寫(xiě)完又思考了一陣,我突然發(fā)現(xiàn)有個(gè)地方考慮得有點(diǎn)多余了。
// 如果在非嚴(yán)格模式下,thisArg的值是null或undefined,需要將thisArg置為全局對(duì)象
if (thisArg === null || thisArg === undefined) {
// 獲取全局對(duì)象時(shí)兼顧瀏覽器環(huán)境和Node環(huán)境
thisArg = (function(){return this}())
} else {
其實(shí)這種情況下不用處理thisArg,因?yàn)榇a執(zhí)行到該函數(shù)后面部分,目標(biāo)函數(shù)會(huì)被作為普通函數(shù)執(zhí)行,那么this自然指向全局對(duì)象!所以這段代碼可以刪除了!
接著來(lái)測(cè)試一下myCall是否可靠,我寫(xiě)了一個(gè)簡(jiǎn)單的例子:
function test(a, b) {
var args = [].slice.myCall(arguments)
console.log(arguments, args)
}
test(1, 2)
var obj = {
name: 'jack'
};
var name = 'global';
function getName() {
return this.name;
}
getName();
getName.myCall(obj);
我不敢保證我寫(xiě)的這個(gè)myCall方法沒(méi)有bug,但也算是考慮了很多情況了。就算是在面試過(guò)程中,面試官主要關(guān)注的就是你的思路和考慮問(wèn)題的全面性,如果寫(xiě)到這個(gè)程度還不能讓面試官滿意,那也無(wú)能為力了......
理解了手寫(xiě)call之后,手寫(xiě)apply也自然觸類(lèi)旁通,只要注意兩點(diǎn)即可。
myApply接受的第二個(gè)參數(shù)是數(shù)組形式。要考慮實(shí)際調(diào)用時(shí)不傳第二個(gè)參數(shù)或者第二個(gè)參數(shù)不是數(shù)組的情況。
直接上代碼:
Function.prototype.myApply = function(thisArg, params) {
var isStrict = (function(){return this === undefined}())
if (!isStrict) {
var thisArgType = typeof thisArg
if (thisArgType === 'number') {
thisArg = new Number(thisArg)
} else if (thisArgType === 'string') {
thisArg = new String(thisArg)
} else if (thisArgType === 'boolean') {
thisArg = new Boolean(thisArg)
}
}
var invokeFunc = this;
// 處理第二個(gè)參數(shù)
var invokeParams = Array.isArray(params) ? params : [];
if (thisArg === null || thisArg === undefined) {
return invokeFunc(...invokeParams)
}
var uniquePropName = Symbol(thisArg)
thisArg[uniquePropName] = invokeFunc
return thisArg[uniquePropName](...invokeParams "uniquePropName")
}
用比較常用的Math.max來(lái)測(cè)試一下:
Math.max.myApply(null, [1, 2, 4, 8]);
// 結(jié)果是8
接下來(lái)就是手寫(xiě)bind了,首先要明確,bind與call, apply的不同點(diǎn)在哪里。
bind返回一個(gè)新的函數(shù)。這個(gè)新的函數(shù)可以預(yù)置參數(shù)。
好的,按照思路開(kāi)始寫(xiě)代碼。
Function.prototype.myBind = function() {
// 保存要綁定的this
var boundThis = arguments[0];
// 獲得預(yù)置參數(shù)
var boundParams = [].slice.call(arguments, 1);
// 獲得綁定的目標(biāo)函數(shù)
var boundTargetFunc = this;
if (typeof boundTargetFunc !== 'function') {
throw new Error('綁定的目標(biāo)必須是函數(shù)')
}
// 返回一個(gè)新的函數(shù)
return function() {
// 獲取執(zhí)行時(shí)傳入的參數(shù)
var restParams = [].slice.call(arguments);
// 合并參數(shù)
var allParams = boundParams.concat(restParams)
// 新函數(shù)被執(zhí)行時(shí),通過(guò)執(zhí)行綁定的目標(biāo)函數(shù)獲得結(jié)果,并返回結(jié)果
return boundTargetFunc.apply(boundThis, allParams)
}
}
本來(lái)寫(xiě)到這覺(jué)得已經(jīng)結(jié)束了,但是翻到一些資料,都提到了手寫(xiě)bind需要支持new調(diào)用。仔細(xì)一想也對(duì),bind返回一個(gè)新的函數(shù),這個(gè)函數(shù)被作為構(gòu)造函數(shù)使用也是很有可能的。
我首先思考的是,能不能直接判斷一個(gè)函數(shù)是不是以構(gòu)造函數(shù)的形式執(zhí)行的呢?如果能判斷出來(lái),那么問(wèn)題就相對(duì)簡(jiǎn)單了。
于是我想到構(gòu)造函數(shù)中很重要的一點(diǎn),那就是在構(gòu)造函數(shù)中,this指向?qū)ο髮?shí)例。所以,我利用instanceof改了一版代碼出來(lái)。
Function.prototype.myBind = function() {
var boundThis = arguments[0];
var boundParams = [].slice.call(arguments, 1);
var boundTargetFunc = this;
if (typeof boundTargetFunc !== 'function') {
throw new Error('綁定的目標(biāo)必須是函數(shù)')
}
function fBound () {
var restParams = [].slice.call(arguments);
var allParams = boundParams.concat(restParams)
// 通過(guò)instanceof判斷this是不是fBound的實(shí)例
var isConstructor = this instanceof fBound;
if (isConstructor) {
// 如果是,說(shuō)明是通過(guò)new調(diào)用的(這里有bug,見(jiàn)下文),那么只要把處理好的參數(shù)傳給綁定的目標(biāo)函數(shù),并通過(guò)new調(diào)用即可。
return new boundTargetFunc(...allParams)
} else {
// 如果不是,說(shuō)明不是通過(guò)new調(diào)用的
return boundTargetFunc.apply(boundThis, allParams)
}
}
return fBound
}
最后看了一下MDN提供的bind函數(shù)的polyfill[5],發(fā)現(xiàn)思路有點(diǎn)不一樣,于是我通過(guò)一個(gè)實(shí)例進(jìn)行對(duì)比。
function test() {}
var fBoundNative = test.bind()
var obj1 = new fBoundNative()
var fBoundMy = test.myBind()
var obj2 = new fBoundMy()
var fBoundMDN = test.mdnBind()
var obj3 = new fBoundMDN()

我發(fā)現(xiàn)我的寫(xiě)法看起來(lái)竟然更像原生bind。瞬間懷疑自己,但一下子卻沒(méi)找到很明顯的bug......
終于我還是意識(shí)到了一個(gè)很大的問(wèn)題,obj1是fBoundNative的實(shí)例,obj3是fBoundMDN的實(shí)例,但obj2不是fBoundMy的實(shí)例(實(shí)際上obj2是test的實(shí)例)。
obj1 instanceof fBoundNative; // true
obj2 instanceof fBoundMy; // false
obj3 instanceof fBoundMDN; // true
存在這個(gè)問(wèn)題麻煩就大了,假設(shè)我要在fBoundMy.prototype上繼續(xù)擴(kuò)展原型屬性或方法,obj2是無(wú)法繼承它們的。所以最直接有效的方法就是用繼承的方法來(lái)實(shí)現(xiàn),雖然不能達(dá)到原生bind的效果,但已經(jīng)夠用了。于是我參考MDN改了一版。
Function.prototype.myBind = function() {
var boundTargetFunc = this;
if (typeof boundTargetFunc !== 'function') {
throw new Error('綁定的目標(biāo)必須是函數(shù)')
}
var boundThis = arguments[0];
var boundParams = [].slice.call(arguments, 1);
function fBound () {
var restParams = [].slice.call(arguments);
var allParams = boundParams.concat(restParams)
return boundTargetFunc.apply(this instanceof fBound ? this : boundThis, allParams)
}
fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)
return fBound
}
這里面最重要的兩點(diǎn):處理好原型鏈關(guān)系,以及理解bind中構(gòu)造實(shí)例的過(guò)程。
原型鏈處理
fBound.prototype = Object.create(boundTargetFunc.prototype || Function.prototype)
這一行代碼中用了一個(gè)||運(yùn)算符,||的兩端充分考慮了myBind函數(shù)的兩種可能的調(diào)用方式。
常規(guī)的函數(shù)綁定
function test(name, age) {
this.name = name;
this.age = age;
}
var bound1 = test.myBind('小明')
var obj1 = new bound1(18)
這種情況把fBound.prototype的原型指向boundTargetFunc.prototype,完全符合我們的思維。
直接使用Function.prototype.myBind
var bound2 = Function.prototype.myBind()
var obj2 = new bound2()
這相當(dāng)于創(chuàng)建一個(gè)新的函數(shù),綁定的目標(biāo)函數(shù)是Function.prototype。這里必然有朋友會(huì)問(wèn)了,Function.prototype也是函數(shù)嗎?是的,請(qǐng)看!
typeof Function.prototype; // "function"
雖然我還不知道第二種調(diào)用方式存在的意義,但是存在即合理,既然存在,我們就支持它。
理解bind中構(gòu)造實(shí)例的過(guò)程
首先要清楚new的執(zhí)行過(guò)程,如果您還不清楚這一點(diǎn),可以看看我寫(xiě)的這篇面試官真的會(huì)問(wèn):new的實(shí)現(xiàn)以及無(wú)new實(shí)例化[4]。
還是之前那句話,先要判斷是不是以構(gòu)造函數(shù)的形式調(diào)用的。核心就是這:
this instanceof fBound
我們用一個(gè)例子再來(lái)分析下new的過(guò)程。
function test(name, age) {
this.name = name;
this.age = age;
}
var bound1 = test.myBind('小明')
var obj1 = new bound1(18)
obj1 instanceof bound1 // true
obj1 instanceof test // true
執(zhí)行構(gòu)造函數(shù) bound1,實(shí)際上是執(zhí)行myBind執(zhí)行后返回的新函數(shù)fBound。首先會(huì)創(chuàng)建一個(gè)新對(duì)象obj1,并且obj1的非標(biāo)準(zhǔn)屬性__proto__指向bound1.prototype,其實(shí)就是myBind執(zhí)行時(shí)聲明的fBound.prototype,而fBound.prototype的原型指向test.prototype。所以到這里,原型鏈就串起來(lái)了!執(zhí)行的構(gòu)造函數(shù)中, this指向這個(gè)obj1。執(zhí)行構(gòu)造函數(shù),由于 fBound是沒(méi)有實(shí)際內(nèi)容的,執(zhí)行構(gòu)造函數(shù)本質(zhì)上還是要去執(zhí)行綁定的那個(gè)目標(biāo)函數(shù),本例中也就是test。因此如果是以構(gòu)造函數(shù)形式調(diào)用,我們就把實(shí)例對(duì)象作為this傳給test.apply。通過(guò)執(zhí)行 test,對(duì)象實(shí)例被掛載了name和age屬性,一個(gè)嶄新的對(duì)象就出爐了!
最后附上Raynos大神寫(xiě)的bind實(shí)現(xiàn)[6],我感覺(jué)又受到了“暴擊”!有興趣鉆研bind終極奧義的朋友請(qǐng)點(diǎn)開(kāi)鏈接查看源碼!

this指向問(wèn)題
分析this的指向,首先要確定當(dāng)前執(zhí)行代碼的環(huán)境。
全局環(huán)境中的this指向
全局環(huán)境中,this指向全局對(duì)象(視宿主環(huán)境而定,瀏覽器是window,Node是global)。
函數(shù)中的this指向
在上文中介紹函數(shù)的調(diào)用形式時(shí)已經(jīng)比較詳細(xì)地說(shuō)過(guò)this指向問(wèn)題了,這里再簡(jiǎn)單總結(jié)一下。
函數(shù)中this的指向取決于函數(shù)的調(diào)用形式,在一些情況下也受到嚴(yán)格模式的影響。
作為普通函數(shù)調(diào)用:嚴(yán)格模式下, this的值是undefined,非嚴(yán)格模式下,this指向全局對(duì)象。作為方法調(diào)用: this指向所屬對(duì)象。作為構(gòu)造函數(shù)調(diào)用: this指向?qū)嵗膶?duì)象。通過(guò)call, apply, bind調(diào)用:如果指定了第一個(gè)參數(shù) thisArg,this的值就是thisArg的值(如果是原始值,會(huì)包裝為對(duì)象);如果不傳thisArg,要判斷嚴(yán)格模式,嚴(yán)格模式下this是undefined,非嚴(yán)格模式下this指向全局對(duì)象。
函數(shù)聲明和函數(shù)表達(dá)式
撕了這么久代碼,讓大腦休息一會(huì)兒,先看點(diǎn)輕松點(diǎn)的內(nèi)容。
函數(shù)聲明
函數(shù)聲明是獨(dú)立的函數(shù)語(yǔ)句。
function test() {}
函數(shù)聲明存在提升(Hoisting)現(xiàn)象,如變量提升一般,對(duì)于同名的情況,函數(shù)聲明優(yōu)于變量聲明(前者覆蓋后者,我說(shuō)的是聲明階段哦)。
函數(shù)表達(dá)式
函數(shù)表達(dá)式不是獨(dú)立的函數(shù)語(yǔ)句,常作為表達(dá)式的一部分,比如賦值表達(dá)式。
函數(shù)表達(dá)式可以是命名的,也可以是匿名的。
// 命名函數(shù)表達(dá)式
var a = function test() {}
// 匿名函數(shù)表達(dá)式
var b = function () {}
匿名函數(shù)就是沒(méi)有函數(shù)名的函數(shù),它不能單獨(dú)使用,只能作為表達(dá)式的一部分使用。匿名函數(shù)常以IIFE(立即執(zhí)行函數(shù)表達(dá)式)的形式使用。
(function(){console.log("我是一個(gè)IIFE")}())
閉包
關(guān)于閉包,我已經(jīng)寫(xiě)了一篇超詳細(xì)的文章去分析了,是個(gè)人原創(chuàng)總結(jié)的干貨,建議直接打開(kāi)解讀閉包,這次從ECMAScript詞法環(huán)境,執(zhí)行上下文說(shuō)起[3]。
PS:閱讀前,您應(yīng)該對(duì)ECMAScript5的一些術(shù)語(yǔ)有一些簡(jiǎn)單的了解,比如Lexical Environment, Execution Context等。
純函數(shù)
純函數(shù)是具備冪等性(對(duì)于相同的參數(shù),任何時(shí)間執(zhí)行純函數(shù)都將得到同樣的結(jié)果),它不會(huì)引起副作用。
純函數(shù)與外部的關(guān)聯(lián)應(yīng)該都來(lái)源于函數(shù)參數(shù)。如果一個(gè)函數(shù)直接依賴了外部變量,那它就不是純函數(shù),因?yàn)橥獠孔兞渴强勺兊?,那么純函?shù)的執(zhí)行結(jié)果就不可控了。
// 純函數(shù)
function pure(a, b) {
return a + b;
}
// 非純函數(shù)
function impure(c) {
return c + d
}
var d = 10;
pure(1, 2); // 3
impure(1); // 11
d = 20;
impure(1); // 21
pure(1, 2); // 3
惰性函數(shù)
相信大家在兼容事件監(jiān)聽(tīng)時(shí),都寫(xiě)過(guò)這樣的代碼。
function addEvent(element, type, handler) {
if (window.addEventListener) {
element.addEventListener(type, handler, false);
} else if (window.attachEvent){
element.attachEvent('on' + type, handler);
} else {
element['on' + type] = handler;
}
}
仔細(xì)看下,我們會(huì)發(fā)現(xiàn),每次調(diào)用addEvent,都會(huì)做一次if-else的判斷,這樣的工作顯然是重復(fù)的。這個(gè)時(shí)候就用到惰性函數(shù)了。
惰性函數(shù)表示函數(shù)執(zhí)行的分支只會(huì)在函數(shù)第一次調(diào)用的時(shí)候執(zhí)行。后續(xù)我們所使用的就是這個(gè)函數(shù)執(zhí)行的結(jié)果。
利用惰性函數(shù)的思維,我們可以改造下上述代碼。
function addEvent(element, type, handler) {
if (window.addEventListener) {
addEvent = function(element, type, handler) {
element.addEventListener(type, handler, false);
}
} else if (window.attachEvent){
addEvent = function(element, type, handler) {
element.attachEvent('on' + type, handler);
}
} else {
addEvent = function(element, type, handler) {
element['on' + type] = handler;
}
}
addEvent(element, type, handler);
}
這代碼看起來(lái)有點(diǎn)low,但是它確實(shí)減少了重復(fù)的判斷。在這種方式下,函數(shù)第一次執(zhí)行時(shí)才確定真正的值。
我們還可以利用IIFE提前確定函數(shù)真正的值。
var addEvent = (function() {
if (window.addEventListener) {
return function(element, type, handler) {
element.addEventListener(type, handler, false);
}
} else if (window.attachEvent){
return function(element, type, handler) {
element.attachEvent('on' + type, handler);
}
} else {
return function(element, type, handler) {
element['on' + type] = handler;
}
}
}())
高階函數(shù)
函數(shù)在javascript中是一等公民,函數(shù)可以作為參數(shù)傳給其他函數(shù),這讓函數(shù)的使用充滿了各種可能性。
不如來(lái)看看維基百科中高階函數(shù)(High-Order Function)的定義:
在數(shù)學(xué)和計(jì)算機(jī)科學(xué)中,高階函數(shù)是至少滿足下列一個(gè)條件的函數(shù):
接受一個(gè)或多個(gè)函數(shù)作為輸入 輸出一個(gè)函數(shù)
看到這,大家應(yīng)該都意識(shí)到了,平時(shí)使用過(guò)很多高階函數(shù)。數(shù)組的一些高階函數(shù)使用得尤為頻繁。
[1, 2, 3, 4].forEach(function(item, index, arr) {
console.log(item, index, arr)
})
[1, 2, 3, 4].map(item => `小老弟 ${item}`)
可以發(fā)現(xiàn),傳入forEach和map的就是一個(gè)函數(shù)。我們自己也可以封裝一些復(fù)用的高階函數(shù)。
我們知道Math.max可以求出參數(shù)列表中最大的值。然而很多時(shí)候,我們需要處理的數(shù)據(jù)并不是1, 2, 3, 4這么簡(jiǎn)單,而是對(duì)象數(shù)組。
假設(shè)有這么一個(gè)需求,存在一個(gè)數(shù)組,數(shù)組元素都是表示人的對(duì)象,我們想從數(shù)組中選出年紀(jì)最大的人。
這個(gè)時(shí)候,就需要一個(gè)高階函數(shù)來(lái)完成。
/**
* 根據(jù)求值條件判斷數(shù)組中最大的項(xiàng)
* @param {Array} arr 數(shù)組
* @param {String|Function} iteratee 返回一個(gè)求值表達(dá)式,可以根據(jù)對(duì)象屬性的值求出最大項(xiàng),比如item.age。也可以通過(guò)自定義函數(shù)返回求值表達(dá)式。
*/
function maxBy(arr, iteratee) {
let values = [];
if (typeof iteratee === 'string') {
values = arr.map(item => item[iteratee]);
} else if (typeof iteratee === 'function') {
values = arr.map((item, index) => {
return iteratee(item, index, arr);
});
}
const maxOne = Math.max(...values);
const maxIndex = values.findIndex(item => item === maxOne);
return arr[maxIndex];
}
利用這個(gè)高階函數(shù),我們就可以求出數(shù)組中年紀(jì)最大的那個(gè)人。
var list = [
{name: '小明', age: 18},
{name: '小紅', age: 19},
{name: '小李', age: 20}
]
// 根據(jù)age字段求出最大項(xiàng),結(jié)果是小李。
var maxItem = maxBy(list, 'age');
我們甚至可以定義更復(fù)雜的求值規(guī)則,比如我們需要根據(jù)一個(gè)字符串類(lèi)型的屬性來(lái)判定優(yōu)先級(jí)。這個(gè)時(shí)候,就必須傳一個(gè)自定義的函數(shù)作為參數(shù)了。
const list = [
{name: '小明', priority: 'middle'},
{name: '小紅', priority: 'low'},
{name: '小李', priority: 'high'}
]
const maxItem = maxBy(list, function(item) {
const { priority } = item
const priorityValue = priority === 'low' ? 1 : priority === 'middle' ? 2 : priority === 'high' ? 3 : 0
return priorityValue;
});
maxBy接受的參數(shù)最終都應(yīng)該能轉(zhuǎn)化為一個(gè)Math.max可度量的值,否則就沒(méi)有可比較性了。
要理解這樣的高階函數(shù),我們可以認(rèn)為傳給高階函數(shù)的函數(shù)就是一個(gè)中間件,它把數(shù)據(jù)預(yù)處理好了,然后再轉(zhuǎn)交給高階函數(shù)繼續(xù)運(yùn)算。
PS:寫(xiě)完這句總結(jié),突然覺(jué)得挺有道理的,反手給自己一個(gè)贊!

柯里化
說(shuō)柯里化之前,首先拋出一個(gè)疑問(wèn),如何實(shí)現(xiàn)一個(gè)add函數(shù),使得這個(gè)add函數(shù)可以靈活調(diào)用和傳參,支持下面的調(diào)用示例呢?
add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
要解答這樣的疑問(wèn),還是要先明白什么是柯里化。
在計(jì)算機(jī)科學(xué)中,柯里化(Currying)是把接受多個(gè)參數(shù)的函數(shù)變換成接受一個(gè)單一參數(shù)(最初函數(shù)的第一個(gè)參數(shù))的函數(shù),并且返回接受余下的參數(shù)且返回結(jié)果的新函數(shù)的技術(shù)。
這段解釋看著還是挺懵逼的,不如舉個(gè)例子:
本來(lái)有這么一個(gè)求和函數(shù)dynamicAdd(),接受任意個(gè)參數(shù)。
function dynamicAdd() {
return [...arguments].reduce((prev, curr) => {
return prev + curr
}, 0)
}
現(xiàn)在需要通過(guò)柯里化把它變成一個(gè)新的函數(shù),這個(gè)新的函數(shù)預(yù)置了第一個(gè)參數(shù),并且可以在調(diào)用時(shí)繼續(xù)傳入剩余參數(shù)。
看到這,我覺(jué)得有點(diǎn)似曾相識(shí),預(yù)置參數(shù)的特性與bind很相像。那么我們不如用bind的思路來(lái)實(shí)現(xiàn)。
function curry(fn, firstArg) {
// 返回一個(gè)新函數(shù)
return function() {
// 新函數(shù)調(diào)用時(shí)會(huì)繼續(xù)傳參
var restArgs = [].slice.call(arguments)
// 參數(shù)合并,通過(guò)apply調(diào)用原函數(shù)
return fn.apply(this, [firstArg, ...restArgs])
}
}
接著我們通過(guò)一些例子來(lái)感受一下柯里化。
// 柯里化,預(yù)置參數(shù)10
var add10 = curry(dynamicAdd, 10)
add10(5); // 15
// 柯里化,預(yù)置參數(shù)20
var add20 = curry(dynamicAdd, 20);
add20(5); // 25
// 也可以對(duì)一個(gè)已經(jīng)柯里化的函數(shù)add10繼續(xù)柯里化,此時(shí)預(yù)置參數(shù)10即可
var anotherAdd20 = curry(add10, 10);
anotherAdd20(5); // 25
可以發(fā)現(xiàn),柯里化是在一個(gè)函數(shù)的基礎(chǔ)上進(jìn)行變換,得到一個(gè)新的預(yù)置了參數(shù)的函數(shù)。最后在調(diào)用新函數(shù)時(shí),實(shí)際上還是會(huì)調(diào)用柯里化前的原函數(shù)。
并且柯里化得到的新函數(shù)可以繼續(xù)被柯里化,這看起來(lái)有點(diǎn)像俄羅斯套娃的感覺(jué)。

實(shí)際使用時(shí)也會(huì)出現(xiàn)柯里化的變體,不局限于只預(yù)置一個(gè)參數(shù)。
function curry(fn) {
// 保存預(yù)置參數(shù)
var presetArgs = [].slice.call(arguments, 1)
// 返回一個(gè)新函數(shù)
return function() {
// 新函數(shù)調(diào)用時(shí)會(huì)繼續(xù)傳參
var restArgs = [].slice.call(arguments)
// 參數(shù)合并,通過(guò)apply調(diào)用原函數(shù)
return fn.apply(this, [...presetArgs, ...restArgs])
}
}
其實(shí)Function.protoype.bind就是一個(gè)柯里化的實(shí)現(xiàn)。不僅如此,很多流行的庫(kù)都大量使用了柯里化的思想。
實(shí)際應(yīng)用中,被柯里化的原函數(shù)的參數(shù)可能是定長(zhǎng)的,也可能是不定長(zhǎng)的。
參數(shù)定長(zhǎng)的柯里化
假設(shè)存在一個(gè)原函數(shù)fn,fn接受三個(gè)參數(shù)a, b, c,那么函數(shù)fn最多被柯里化三次(有效地綁定參數(shù)算一次)。
function fn(a, b, c) {
return a + b + c
}
var c1 = curry(fn, 1);
var c2 = curry(c1, 2);
var c3 = curry(c2, 3);
c3(); // 6
// 再次柯里化也沒(méi)有意義,原函數(shù)只需要三個(gè)參數(shù)
var c4 = curry(c3, 4);
c4();
也就是說(shuō),我們可以通過(guò)柯里化緩存的參數(shù)數(shù)量,來(lái)判斷是否到達(dá)了執(zhí)行時(shí)機(jī)。那么我們就得到了一個(gè)柯里化的通用模式。
function curry(fn) {
// 獲取原函數(shù)的參數(shù)長(zhǎng)度
const argLen = fn.length;
// 保存預(yù)置參數(shù)
const presetArgs = [].slice.call(arguments, 1)
// 返回一個(gè)新函數(shù)
return function() {
// 新函數(shù)調(diào)用時(shí)會(huì)繼續(xù)傳參
const restArgs = [].slice.call(arguments)
const allArgs = [...presetArgs, ...restArgs]
if (allArgs.length >= argLen) {
// 如果參數(shù)夠了,就執(zhí)行原函數(shù)
return fn.apply(this, allArgs)
} else {
// 否則繼續(xù)柯里化
return curry.call(null, fn, ...allArgs)
}
}
}
這樣一來(lái),我們的寫(xiě)法就可以支持以下形式。
function fn(a, b, c) {
return a + b + c;
}
var curried = curry(fn);
curried(1, 2, 3); // 6
curried(1, 2)(3); // 6
curried(1)(2, 3); // 6
curried(1)(2)(3); // 6
curried(7)(8)(9); // 24
參數(shù)不定長(zhǎng)的柯里化
解決了上面的問(wèn)題,我們難免會(huì)問(wèn)自己,假設(shè)原函數(shù)的參數(shù)不定長(zhǎng)呢,這種情況如何柯里化?
首先,我們需要理解參數(shù)不定長(zhǎng)是指函數(shù)聲明時(shí)不約定具體的參數(shù),而在函數(shù)體中通過(guò)arguments獲取實(shí)參,然后進(jìn)行運(yùn)算。就像下面這種。
function dynamicAdd() {
return [...arguments].reduce((prev, curr) => {
return prev + curr
}, 0)
}
回到最開(kāi)始的問(wèn)題,怎么支持下面的所有調(diào)用形式?
add(1, 2, 3) // 6
add(1) // 1
add(1)(2) // 3
add(1, 2)(3) // 6
add(1)(2)(3) // 6
add(1)(2)(3)(4) // 10
思考了一陣,我發(fā)現(xiàn)在參數(shù)不定長(zhǎng)的情況下,要同時(shí)支持1~N次調(diào)用還是挺難的。add(1)在一次調(diào)用后可以直接返回一個(gè)值,但它也可以作為函數(shù)接著調(diào)用add(1)(2),甚至可以繼續(xù)add(1)(2)(3)。那么我們實(shí)現(xiàn)add函數(shù)時(shí),到底是返回一個(gè)函數(shù),還是返回一個(gè)值呢?這讓人挺犯難的,我也不能預(yù)測(cè)這個(gè)函數(shù)將如何被調(diào)用啊。
而且我們可以拿上面的成果來(lái)驗(yàn)證下:
curried(1)(2)(3)(4);
運(yùn)行上面的代碼會(huì)報(bào)錯(cuò):Uncaught TypeError: curried(...)(...)(...) is not a function,因?yàn)閳?zhí)行到curried(1)(2)(3),結(jié)果就不是一個(gè)函數(shù)了,而是一個(gè)值,一個(gè)值當(dāng)然是不能作為函數(shù)繼續(xù)執(zhí)行的。
所以如果要支持參數(shù)不定長(zhǎng)的場(chǎng)景,已經(jīng)柯里化的函數(shù)在執(zhí)行完畢時(shí)不能返回一個(gè)值,只能返回一個(gè)函數(shù);同時(shí)要讓JS引擎在解析得到的這個(gè)結(jié)果時(shí),能求出我們預(yù)期的值。
大家看了這個(gè)可能還是不懂,好,說(shuō)人話!我們實(shí)現(xiàn)的curry應(yīng)該滿足:
經(jīng) curry處理,得到一個(gè)新函數(shù),這一點(diǎn)不變。
// curry是一個(gè)函數(shù)
var curried = curry(add);
新函數(shù)執(zhí)行后仍然返回一個(gè)結(jié)果函數(shù)。
// curried10也是一個(gè)函數(shù)
var curried10 = curried(10);
var curried30 = curried10(20);
結(jié)果函數(shù)可以被Javascript引擎解析,得到一個(gè)預(yù)期的值。
curried10; // 10
好,關(guān)鍵點(diǎn)在于3,如何讓Javascript引擎按我們的預(yù)期進(jìn)行解析,這就回到Javascript基礎(chǔ)了。在解析一個(gè)函數(shù)的原始值時(shí),會(huì)用到toString。
我們知道,console.log(fn)可以把函數(shù)fn的源碼輸出,如下所示:
console.log(fn)
? fn(a, b, c) {
return a + b + c;
}
那么我們只要重寫(xiě)toString,就可以巧妙地實(shí)現(xiàn)我們的需求了。
function curry(fn) {
// 保存預(yù)置參數(shù)
const presetArgs = [].slice.call(arguments, 1)
// 返回一個(gè)新函數(shù)
function curried () {
// 新函數(shù)調(diào)用時(shí)會(huì)繼續(xù)傳參
const restArgs = [].slice.call(arguments)
const allArgs = [...presetArgs, ...restArgs]
return curry.call(null, fn, ...allArgs)
}
// 重寫(xiě)toString
curried.toString = function() {
return fn.apply(null, presetArgs)
}
return curried;
}
這樣一來(lái),魔性的add用法就都被支持了。
function dynamicAdd() {
return [...arguments].reduce((prev, curr) => {
return prev + curr
}, 0)
}
var add = curry(dynamicAdd);
add(1)(2)(3)(4) // 10
add(1, 2)(3, 4)(5, 6) // 21
至于為什么是重寫(xiě)toString,而不是重寫(xiě)valueOf,這里留個(gè)懸念,大家可以想一想,也歡迎與我交流!
柯里化總結(jié)
柯里化是一種函數(shù)式編程思想,實(shí)際上在項(xiàng)目中可能用得少,或者說(shuō)用得不深入,但是如果你掌握了這種思想,也許在未來(lái)的某個(gè)時(shí)間點(diǎn),你會(huì)用得上!
大概來(lái)說(shuō),柯里化有如下特點(diǎn):
簡(jiǎn)潔代碼:柯里化應(yīng)用在較復(fù)雜的場(chǎng)景中,有簡(jiǎn)潔代碼,可讀性高的優(yōu)點(diǎn)。 參數(shù)復(fù)用:公共的參數(shù)已經(jīng)通過(guò)柯里化預(yù)置了。 延遲執(zhí)行:柯里化時(shí)只是返回一個(gè)預(yù)置參數(shù)的新函數(shù),并沒(méi)有立刻執(zhí)行,實(shí)際上在滿足條件后才會(huì)執(zhí)行。 管道式流水線編程:利于使用函數(shù)組裝管道式的流水線工序,不污染原函數(shù)。
小結(jié)
本文是筆者回顧函數(shù)知識(shí)點(diǎn)時(shí)總結(jié)的一篇非常詳細(xì)的文章。在理解一些晦澀的知識(shí)模塊時(shí),我加入了一些個(gè)人解讀,相信對(duì)于想要深究細(xì)節(jié)的朋友會(huì)有一些幫助。如果您覺(jué)得這篇文章有所幫助,請(qǐng)無(wú)情地關(guān)注點(diǎn)贊支持一下吧
參考
「思維導(dǎo)圖學(xué)前端 」6k字一文搞懂Javascript對(duì)象,原型,繼承: https://juejin.im/post/6844904194097299463
[2]「思維導(dǎo)圖學(xué)前端 」初中級(jí)前端值得收藏的正則表達(dá)式知識(shí)點(diǎn)掃盲: https://juejin.im/post/6850037267365855239
[3]解讀閉包,這次從ECMAScript詞法環(huán)境,執(zhí)行上下文說(shuō)起: https://juejin.im/post/6858052418862235656
[4]面試官真的會(huì)問(wèn):new的實(shí)現(xiàn)以及無(wú)new實(shí)例化: https://juejin.im/post/6850037282319204360
[5]bind函數(shù)的polyfill: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Function/bind
[6]Raynos大神寫(xiě)的bind實(shí)現(xiàn): https://github.com/Raynos/function-bind/blob/master/implementation.js
