最近在學(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 會打印出this
的name
举畸,但是 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ù)的this
被bind
的第一個參數(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访递。碼字不易鞋既,歡迎點贊。