一直以來沒有對函數式編程有一個全面的學習和使用,或者說沒有一個深入的思考。最近看到一些博客文章,忽然以爲函數式編程仍是蠻有意思的。看了些書和文章。這裏記載下感悟和收穫。 歡迎團隊姜某人多多指點@姜少。 因爲博客秉持着簡短且全面原則。遂分爲上下兩篇javascript
函數的第一原則是要小,函數的第二原則是要更小java
在理解什麼是函數式編程的開始,咱們先了解下什麼數學中,函數具備的特性node
因此咱們說,函數式編程是一種範式,咱們可以以此建立僅依賴輸入就能夠完成自身邏輯的函數。這保證了當函數屢次調用時,依然能夠返回相同的結果。所以能夠產生可緩存的、可測試的代碼庫git
全部的函數對於相同的輸入都返回相同的結構,這一特性,咱們稱之爲引用透明。 好比:es6
let identity = (i) => {return i};
複製代碼
這麼簡單?對,其實就是這樣,也就是說他沒有依賴任何外部變量、外部環境,只要你給我東西,我通過一頓鼓搗,老是給你返回你所能預測的結果。github
這也爲咱們後面的併發代碼、緩存成爲可能。編程
函數式編程主張聲明式編程和編寫抽象代碼。其實這個比較有意思,感受更像是面向對象的編程。redux
光說不練都是扯淡。舉個栗子數組
var array = [1,2,3,4,5,6];
for(let i = 0;i<array.length;i++){
console.log(array[i])
}
複製代碼
這段代碼的做用簡單明瞭,就是遍歷!可是你有沒有感受這個代碼呆呆的。沒有一丁點的靈氣?都是我告訴你該怎麼該怎麼作的。咱們告訴編譯器,你先去獲取下數組的長度的,而後挨個log出來。這種編碼方式,咱們一般稱之爲「命令式」解決方案。
而在函數式編程中,咱們其實更加主張用「聲明式」解決方案
let array = [1,2,3,4,5,6];
array.forEach(item=>{console.log(item)})
複製代碼
簡單體會下,是否是有那麼一丟丟的靈感來了?等等,你這個forEach函數哪來的嘛!對,也是本身寫的,可是不是咱們經過編寫這種抽象邏輯代碼,而讓總體的業務代碼更加的清晰明瞭了呢?開發者是須要關心手頭上的問題就行了,只須要告訴編譯器去幹嗎而不是怎麼幹了。是否是輕鬆了?
其實函數式編程主張的就是以抽象的方式建立函數。這些函數能夠在代碼的其餘部分被重用。
好處我的不喜歡扯太多,不是由於他沒有好處,而是對於剛剛接觸函數式編程的哥們,上來就說好處實際上是沒什麼概念的,因此這裏我簡單提一提,後面文章會細細說明。
熟悉redux的同窗應該對這個詞語都不陌生,所謂的純函數,其實也就是咱們說的引用透明,穩定輸出!好處呢?可預測嘛,容易編寫測試代碼哇,可緩存嘛。什麼是可緩存?能夠看我以前發的文章哈,這裏簡單舉個栗子
let longRunningFunction = (input)=>{
//進行了很是麻煩的計算,而後返回出來結果
return output;
}
複製代碼
若是longRunningFunction是一個純函數,引用透明。咱們就能夠說對於一樣的輸出,老是返回一樣的結果,因此咱們爲何不可以運用一個對象將咱們每一次的運算結果存起來呢?
let longRunningFunctionResult = {1:2,2:3,3:4};
//檢查key是否存在,存在直接用,不存在再計算
longRunningFunctionResult.hasOwnProperty(input)?longRunningFunctionResult[input]:longRunningFunctionResult[input] = longRunningFunction(input)
複製代碼
比較直觀。很少說了哈。其實好處還有以前說到的併發。不說的這麼堂而皇之了,啥併不併發呀,我不依賴別人的任何因素,只依據你的輸出我產出。你說我支持什麼就是什麼咯,只要你給我對的參數傳進來就能夠了。
匆匆收尾!僅做爲拋磚引玉。後面我們在系統性的學習下函數式編程。
JavaScript做爲一門語言,將函數視爲數據。容許函數代替數據傳遞是一個很是強大的概念。接受一個函數做爲參數的函數成爲高階函數(Higher-Order Function)
JavaScript支持以下幾種數據類型:
這裏面想強調的是JavaScript將函數也一樣是爲一種數據類型。當一門語言容許將函數做爲數據那樣傳遞和使用的時候,咱們就稱函數爲一等公民。
因此說這個就是爲了強調說明,在JavaScript中,函數能夠被賦值,做爲參數傳遞,也能夠被其餘函數返回。
//傳遞函數
let tellType = (arg)=>{
if(typeof arg === 'function'){
arg();
}else{
console.log(`this data is ${arg}`)
}
}
let dataFn = ()=> {
console.log('this is a Function');
}
tellType(dataFn);
複製代碼
//返回函數
let returnStr = ()=> String;
returnStr()('Nealyang')
//let fn = returnStr();
//fn('Nealyang');
複製代碼
從上咱們能夠看到函數能夠接受另外一個函數做爲參數,一樣,函數也能夠將兩一個函數做爲返回值返回。
因此高階函數就是接受函數做爲參數而且/或者返回函數做爲輸出的函數
當咱們瞭解到如何去建立並執行一個高階函數的時候,同行咱們都想去了解,他究竟是幹嗎的?OK,簡單的說,高階函數經常使用於抽象通用的問題。換句話說,高階函數就是定義抽象。簡單的說,其實就相似於命令式的編程方式,將具體的實現細節封裝、抽象起來,讓開發者更加的關心業務。抽象讓咱們專一於預約的目標而不是去關心底層的系統概念。
理解這個概念很是重要,因此下面咱們將經過大量的栗子來講明
const every = (arr,fn)=>{
let result = true;
for(const value of arr){
result = result && fn(value);
}
return result;
}
every([NaN,NaN,4],isNaN);
const some = (arr,fn)=>{
let result = true;
for(const value of arr){
result = result || fn(value);
}
return result;
}
some([3,1,2],isNaN);
//這裏都是低效的實現。這裏主要是理解高階函數的概念
複製代碼
let sortObj = [
{firstName:'aYang',lastName:'dNeal'},
{firstName:'bYang',lastName:'cNeal'},
{firstName:'cYang',lastName:'bNeal'},
{firstName:'dYang',lastName:'aNeal'},
];
const sortBy = (property)=>{
return (a,b) => {
return (a[property]<b[property])?-1:(a[property]>b[property])?1:0
}
}
sortObj.sort(sortBy('lastName'));
//sort函數接受了被sortBy函數返回的比較函數,咱們再次抽象出compareFunction的邏輯,讓用戶更加關注比較,而不用去在意怎麼比較的。
複製代碼
上面的sortBy其實你們都應該看到了閉包的蹤跡。關於閉包的產生、概念這裏就不囉嗦了。總之咱們知道,閉包很是強大的緣由就是它對做用域的訪問。
簡單說下閉包的三個可訪問的做用域:
const forEach = (arr,fn)=>{
for(const item of arr){
fn(item);
}
}
//tap接受一個value,返回一個帶有value的閉包函數
const tap = (value)=>(fn)=>{
typeof fn === 'function'?fn(value):console.log(value);
}
forEach([1,2,3,4,5],(a)=>{
tap(a)(()=>{
console.log(`Nealyang:${a}`)
})
});
複製代碼
直接看概念,柯里化是把一個多參函數轉換爲一個嵌套的一元函數的過程
不理解,莫方!舉個栗子就明白了。
假設咱們有一個函數,add:
const add = (x,y)=>x+y;
複製代碼
咱們調用的時候固然就是add(1,2),沒有什麼特別的。當咱們柯里化了之後呢,就是以下版本:
const addCurried = x => y => x + y;
複製代碼
調用的時候呢,就是這個樣子的:
addCurried(4)(4)//8
複製代碼
是否是很是的簡單?
說到這,咱們在來回顧下,柯里化的概念:把一個多參函數轉換成一個嵌套的一元函數的過程。
上面的代碼中,咱們實現了二元函數轉爲一元函數的過程。那麼對於多參咱們該如何作呢?
這個是比較重要的部分,咱們一步一步來實現
咱們先來添加一個規則,最一層函數檢查,若是傳入的不是一個函數來調用curry函數則拋出錯誤。當若是提供了柯里化函數的全部參數,則經過使用這些傳入的參數調用真正的函數。
let curry = (fn) => {
if(typeof fn !== 'function'){
throw Error('not a function');
}
return function curriedFn (...args){
return fn.apply(null,args);
}
}
複製代碼
因此如上,咱們就能夠這麼玩了
const multiply = (x,y,z) => x * y * z;
curry(multiply)(1,2,3);//6
複製代碼
革命還未成功,咱們繼續哈~下面咱們的目的就是把多參函數轉爲嵌套的一元函數(重回概念)
const multiply = (x,y,z) => x * y * z;
let curry = (fn) => {
if(typeof fn !== 'function'){
throw Error('not a function');
}
return function curriedFn (...args){
if(args.length < fn.length){
return function(){
return curriedFn.apply(null,args.concat([].slice.call(arguments)));
}
}
return fn.apply(null,args);
}
}
curry(multiply)(1)(2)(3)
複製代碼
若是是初次看到,可能會有些疑惑。咱們一行行來瞅瞅。
args.length < fn.length
複製代碼
這段代碼比價直接,就是判斷,你傳入的參數是否小於函數參數長度。
args.concat([].slice.call(arguments))
複製代碼
咱們使用cancat函數連接一次傳入的一個參數,並遞歸調用curriedFn。因爲咱們將全部的參數傳入組合並遞歸調用,最終if判斷會失效,就返回結果了。
####小小實操一下 咱們寫一個函數在數組內容中查找到包含數字的項
let curry = (fn) => {
if(typeof fn !== 'function'){
throw Error('not a function');
}
return function curriedFn (...args){
if(args.length < fn.length){
return function(){
return curriedFn.apply(null,args.concat([].slice.call(arguments)));
}
}
return fn.apply(null,args);
}
}
let match = curry(function(expr,str){return str.match(expr)});
let hasNumber = match(/[0-9]+/);
let filter = curry(function(f,ary){
return ary.filter(f)
});
filter(hasNumber)(['js','number1']);
複製代碼
經過如上的例子,我想咱們也應該看出來,爲何咱們須要函數的柯里化:
假設咱們須要10ms後執行某一個特定操做,咱們通常的作法是
setTimeout(() => console.log('do something'),10);
setTimeout(() => console.log('do other thing'),10);
複製代碼
如上,咱們調用函數都傳入了10,能使用curry函數把他在代碼中隱藏嗎?我擦,咱curry多牛逼!確定不行的嘛~
由於curry函數應用參數列表是從最左到最右的。因爲咱們是根據須要傳遞函數,並將10保存在常量中,因此不能以這種方式使用curry。咱們能夠這麼作:
const setTimeoutFunction = (time , fn) => {
setTimeout(fn,time);
}
複製代碼
可是若是這樣的話,咱們是否是太過於麻煩了呢?爲了減小了10的傳遞,還須要多造一個包裝函數?
這時候,偏應用就出來了!!!
簡單看下代碼實現:
const partial = function (fn,...partialArgs){
let args = partialArgs;
return function(...fullArgs){
let arg = 0;
for(let i = 0; i<args.length && fullArgs.length;i++){
if(arg[i] === undefined){
args[i] = fullArgs[arg++];
}
}
return fn.apply(null,args)
}
}
let delayTenMs = partial(setTimeout , undefined , 10);
delayTenMs(() => console.log('this is Nealyang'));
複製代碼
如上你們應該都可以理解。這裏不作過多廢話解釋了。
簡單總結的說:
因此,像map,filter咱們能夠輕鬆的使用curry函數解決問題,可是對於setTimeout這類,最合適的選擇固然就是偏函數了。總之,咱們使用curry或者partial是爲了讓函數參數或者函數設置變得更加的簡單強大。
上一部分說的比較淺顯基礎,但願你們也可以從中感覺到函數式編程的精妙和靈活之處。大神請直接略過~求指正求指導~
下一節中,將主要介紹下,函數式編程中的組合、管道、函子以及Monad。最後咱們在介紹下es6的Generator,或許咱們能從最後的Generator中豁然開朗得到到不少啓發哦~~
nodejs 技術交流 羣號:698239345
React技術棧羣號:398240621
前端技術雜談羣號:604953717