JS 高階函數(shù)

最近在學(xué)習(xí)函數(shù)式編程妥衣,整個 team 都在啃一本叫《Mostly adequate guide》的函數(shù)式編程教材,難度確實挺大的税手,不過新意滿滿。今天就講講 FP 基礎(chǔ)中的基礎(chǔ)——高階函數(shù)狂票。

Function Object

什么是函數(shù)熙暴?在大多數(shù)編程語言中慌盯,函數(shù)是一段獨立的代碼塊,用來處理某些通用功能的方法俱箱;主要操作是給函數(shù)傳入特定對象(參數(shù))灭必,并在方法調(diào)用結(jié)束后獲得一個新的對象(返回值)。

function greeting(name) {
  return `Hello ${name}`;
}

console.log( greeting('Onion') ); // Hello Onion

但是在 Javascript禁漓、Haskell播歼、Clojure 這類語言中,函數(shù)是另一種更高級的存在秘狞,俗稱一等公民;它除了是代碼塊以外雇初,它還是一種特殊類型的對象——Function Object减响。

為什么說 Fuction 也是對象呢?還是看上面的示例函數(shù)——greeting呻畸,我們事實上是可以打印出它的固有屬性(properties)的:

console.log(greeting.length, greeting.name);  // 1 'greeting'

這里length是參數(shù)列表長度悼院,name就是它定義的名字了。是不是和對象很接近了?我們甚至可以給它添加新的屬性和方法:

greeting.displayName = 'Garlic';
greeting.innerName = () => 'Ginger';

console.log(greeting.displayName); // Garlic
console.log(greeting.innerName()); // Ginger

是吧叙甸?這么看位衩,函數(shù)已經(jīng)包含了幾乎所有的 Object 功能了。當然僚祷,生產(chǎn)中盡量不要給函數(shù)添加隨機屬性贮缕,畢竟代碼是給人閱讀的,不要隨便增加團隊的認知成本感昼。

high order function

上面提到了函數(shù)是一種特殊的對象定嗓,因此在 js 語言中橘蜜,函數(shù)也可以像普通 object 一樣成為其他函數(shù)里的參數(shù)或是返回值。我們將參數(shù)或是返回值為函數(shù)的函數(shù)稱為高階函數(shù)

Higher-Order function is a function that receives a function as an argument or returns the function as output

Function 參數(shù)

先看一下函數(shù)參數(shù)的用法涯贞,最經(jīng)典的案例就是 Array#map萝招。給個例子辛块,實現(xiàn)一個讓數(shù)組所有元素+1 的操作,傳統(tǒng)的做法如下所示:

const arr1 = [1, 2, 3];
const arr2 = [];

for(let i = 0; i < arr1.length; i++) {
  arr2.push(++arr1[i]);
}
console.log(arr2)

如果使用高階函數(shù) map:

const arr1 = [1, 2, 3];

const arr3 = arr1.map( function callback(element, index, array) {
  return element+1;
});

console.log(arr3); // [2, 3, 4]

map 是 Array.prototype 的原生方法线椰,它的第一個參數(shù)是一個 callback 函數(shù)尘盼,第二個參數(shù)是用來綁定 callback 的 this卿捎。這里,callback 的作用是迭代調(diào)用數(shù)組里的元素午阵,并將返回值組裝成一個新的數(shù)組享扔。這個 map 的函數(shù)參數(shù)本身還有三個參數(shù):element植袍,index 和 array,分別表示迭代時的元素氛魁,索引厅篓,以及原始數(shù)組。

上面的代碼使用 es6 的箭頭函數(shù)应又,可以寫得更簡潔一點:

const arr1 = [1, 2, 3];

const arr3 = arr1.map(e => e+1);

console.log(arr3); // [2, 3, 4]

講真乏苦,我們經(jīng)常用到高階函數(shù)尤筐,Array 里還有好多類似的函數(shù),如 fliter掀淘、reduce 等等油昂。這類高階函數(shù)可以明顯的改善代碼質(zhì)量,并切能確保不會對原始數(shù)組產(chǎn)生副作用冕碟。

Fucntion 返回值

返回值是函數(shù)的函數(shù),我們也經(jīng)常使用厕妖,最著名的就是 Function#bind挑庶。

給個案例,如下函數(shù) greeting 會打印出thisname举畸,但是 greeting 并不是一個純函數(shù)凳枝,因為它的 this 綁定不明確,可能會在不同的運行上下文中會返回不同的結(jié)果合是。

function greeting() {
  return `Hello ${this.name}`;
}

如果想明確它的結(jié)果該怎么辦呢?嗯聪全,為 greeting 綁定一個 object。這個 helloOnion 就是greeting.bind后返回的新函數(shù)娃圆。

let helloOnoin = greeting.bind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onion

bind方法創(chuàng)建一個新的函數(shù)蛾茉,在bind被調(diào)用時,這個新函數(shù)的thisbind的第一個參數(shù)指定悦屏,其余的參數(shù)將作為新函數(shù)的參數(shù)供調(diào)用時使用键思。我們可以試著寫一個乞丐版的 myBind 方法(bind 還能綁定參數(shù),這個先略過了)看蚜,這樣可以更清晰地看到什么是返回函數(shù)的高階函數(shù)了赔桌。

Function.prototype.myBind = function(context) {
  let func = this; // method is attached to the prototype, so just refer to it as this.
  return function newFn() {
    return func.apply(context, arguments);
  }
}

這里給 Function 的原型鏈加了一個新的函數(shù) myBind,并用到了閉包(在內(nèi)存里保留了原始函數(shù)和目標this)音诫;之后雪位,調(diào)用 myBind 返回一個新的函數(shù),并且在該函數(shù)運行時調(diào)用原始函數(shù)茧泪,最后apply執(zhí)行時綁定目標 this⊙ù担看一下效果:

let helloOnoin = greeting.myBind({name: 'Onion'});

console.log(helloOnoin()); // Hello Onoin

我這里再寫一個健壯一點的 bind 實現(xiàn)嗜侮,大家自己體會一下啥容,bind 是如何將前幾個參數(shù)也綁定了的:

Function.prototype.bind = function(context, ...args) {
  let func = this;
  return function () {
    return func.call(context, ...args, ...arguments);
  }
}

函數(shù)柯里化

高階函數(shù)還在一種叫柯里化的方法里大顯身手咪惠。

在數(shù)學(xué)和計算機科學(xué)中淋淀,柯里化是一種將使用多個參數(shù)的函數(shù)轉(zhuǎn)換成一系列使用一個參數(shù)的函數(shù),并且返回接受余下的參數(shù)而且返回結(jié)果的新函數(shù)的技術(shù)炭臭。

柯里化袍辞,通俗點說就是先給原始函數(shù)傳入幾個參數(shù),它會生成一個新的函數(shù)搅吁,然后讓新的函數(shù)去處理接下來的參數(shù)。我們先不去管 curry 的實現(xiàn)肚豺,看看柯里函數(shù)的用法党瓮。比如盐类,實現(xiàn)一個 add 函數(shù)——簡單的兩數(shù)相加,常規(guī)手斷就是直接加兩參數(shù)運行——add(1,2)枪萄。但是這里我們先給它做個柯里化處理猫妙,并產(chǎn)生了一個新的函數(shù)——curryingAdd。

function curry(fn) { ... }

function add(a, b) { return a+b; }

const curryingAdd = curry(add);

柯里化后的 curryingAdd齐帚,從普通函數(shù)變成了高階函數(shù):它支持一次傳入一個參數(shù)(比如 10)并返回一個新的函數(shù)——addTen彼哼。我們運行addTen(1),它會記錄之前已經(jīng)傳入的 10剪菱,并把 10 和 1 相加得到 11。是不是覺得很沒用孝常?哈,這說明你 FP 學(xué)的不夠深上渴,在FP里所有的函數(shù)都是柯里話了的冻押,所有函數(shù)都是可以延遲計算的。

const addTen = curryAdd(10);

console.log(addTen(1)); // 11
console.log(addTen(100)); // 110

柯里化的作用就是將普通函數(shù)轉(zhuǎn)變成高階函數(shù)括袒,實現(xiàn)動態(tài)創(chuàng)建函數(shù)稿茉、延遲計算、參數(shù)復(fù)用等等作用恃慧。篇幅有限渺蒿,我不做深入講解了。實現(xiàn)上怠蹂,就是返回一個高階函數(shù)少态,通過閉包把傳入的參數(shù)保存起來。當傳入的參數(shù)數(shù)量不足時彼妻,遞歸調(diào)用 bind 方法;數(shù)量足夠時則立即執(zhí)行函數(shù)屋摇。學(xué)習(xí)一下 javascript 的高階用法還是有意義的幽邓。

function curry(fn) {
  const arity = fn.length;

  return function $curry(...args) {
    if( args.length < arity ) {
      return $curry.bind(null, ...args);
    }
    return fn.apply(null, args);
  }
}

compose

compose 也是一個高階函數(shù)里重要的一課颊艳。compose 就是組合函數(shù)忘分,將子函數(shù)串聯(lián)起來執(zhí)行白修,一個函數(shù)的輸出結(jié)果是另一個函數(shù)的輸入?yún)?shù),一旦第一個函數(shù)開始執(zhí)行兵睛,會像多米諾骨牌一樣推導(dǎo)執(zhí)行后續(xù)函數(shù)祖很。還是舉個例子:我實現(xiàn)了一個帶 Hello 的greeting函數(shù),并希望在greeting調(diào)用結(jié)束后把返回值都顯示成大寫狀態(tài)假颇。

const greeting = name => `Hello ${name}`;
const toUpper = str => str.toUpperCase();

toUpper(greeting('Onion')); // HELLO ONION

傳統(tǒng)的手段就是嵌套兩個函數(shù)使用——toUpper(greeting('Onion'))笨鸡,但是有時候這種嵌套可能會很多,比如下面這個態(tài)勢:

f(g(h(i(j(k('Onion'))))))

再看看 compose 的用法:

const composedFn = compose(f, g, h, i, j, k)
console.log( composedFn('Onion') )

是不是這一個 composedFn 函數(shù)比那種一層層的嵌套要美觀得多哥桥?OK激涤,怎么實現(xiàn) compose 函數(shù)呢?把源碼貼在這里了倦踢。如果你覺得寫(...fns) => (...args) => ..這類代碼不可思議的話硼一,建議啃一下上面提到的教材《Mostly adequate guide》梦抢,啃完你就發(fā)現(xiàn)再正常不過了。

// compose: ( (a->b), (b->c), ..., (y->z) ) -> a -> z
const compose = (...fns) => (...args) => fns.reduceRight((res, fn) => [fn.apply(null, res)], args)[0];

小結(jié)

這一期快速科普了 JS 高階函數(shù)哼蛆,現(xiàn)實開發(fā)中很多人都覺得沒啥用霞赫,但是面試官很喜歡問這類問題。倒不是說面試官懂很多端衰,大概率他也只是看題庫問問題罷了。我是覺得學(xué)習(xí)這類方法的意義還是在于思維訓(xùn)練——為 FP 編程打好基礎(chǔ)灭抑;相傳,F(xiàn)P 開發(fā)人員的收入是普通的三倍忘嫉。為了成為一個更“有錢”的開發(fā)人員案腺,共勉。

相關(guān)

文章同步發(fā)布于an-Onion 的 Github访递。碼字不易鞋既,歡迎點贊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末跌前,一起剝皮案震驚了整個濱河市陡舅,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌灾炭,老刑警劉巖颅眶,帶你破解...
    沈念sama閱讀 218,546評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件涛酗,死亡現(xiàn)場離奇詭異,居然都是意外死亡商叹,警方通過查閱死者的電腦和手機剖笙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來过蹂,“玉大人,你說我怎么就攤上這事酷勺。” “怎么了勋功?”我有些...
    開封第一講書人閱讀 164,911評論 0 354
  • 文/不壞的土叔 我叫張陵狂鞋,是天一觀的道長潜的。 經(jīng)常有香客問我,道長啰挪,這世上最難降的妖魔是什么亡呵? 我笑而不...
    開封第一講書人閱讀 58,737評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮下硕,結(jié)果婚禮上汁胆,老公的妹妹穿的比我還像新娘。我一直安慰自己誉尖,他們只是感情好铸题,可當我...
    茶點故事閱讀 67,753評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著没咙,像睡著了一般千劈。 火紅的嫁衣襯著肌膚如雪牌捷。 梳的紋絲不亂的頭發(fā)上涡驮,一...
    開封第一講書人閱讀 51,598評論 1 305
  • 那天捉捅,我揣著相機與錄音虽风,去河邊找鬼。 笑死无牵,一個胖子當著我的面吹牛厂抖,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播忱辅,決...
    沈念sama閱讀 40,338評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼墙懂,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了蒜魄?” 一聲冷哼從身側(cè)響起场躯,我...
    開封第一講書人閱讀 39,249評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎伞鲫,沒想到半個月后签舞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,696評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡吠架,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,888評論 3 336
  • 正文 我和宋清朗相戀三年傍药,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拣挪。...
    茶點故事閱讀 40,013評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡俱诸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出赶诊,到底是詐尸還是另有隱情园骆,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評論 5 346
  • 正文 年R本政府宣布辙喂,位于F島的核電站鸠珠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏炬太。R本人自食惡果不足惜驯耻,卻給世界環(huán)境...
    茶點故事閱讀 41,348評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望霎迫。 院中可真熱鬧帘靡,春花似錦、人聲如沸描姚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽绊寻。三九已至,卻和暖如春观游,著一層夾襖步出監(jiān)牢的瞬間驮俗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評論 1 270
  • 我被黑心中介騙來泰國打工搪柑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留索烹,地道東北人工碾。 一個月前我還...
    沈念sama閱讀 48,203評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像百姓,于是被迫代替她去往敵國和親渊额。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,960評論 2 355

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