框架總覽
?? 引言
?? 什么是函數(shù)式編程?
-
?? 函數(shù)是純函數(shù)
- ?? 什么是純函數(shù)
- ?? 函數(shù)的副作用
- ?? 使用純函數(shù)的優(yōu)點
- ?? 可測試性
- ?? 可緩存性
- ?? 可移植性
- ?? 純函數(shù)的特點
- ?? 不可變性
- ?? 引用透明
- ?? 聲明式編程
-
?? 函數(shù)是第一等公民
- ?? 閉包
- ?? 高階函數(shù)
- ?? 函數(shù)柯里化(Currying)
- ?? 函數(shù)合成(compose)
?? 結(jié)合使用
?? 擴展
1.引言
函數(shù)式編程的歷史已經(jīng)很悠久了,但是最近幾年卻頻繁的出現(xiàn)在大眾的視野,很多不支持函數(shù)式編程的語言也在積極加入閉包,匿名函數(shù)等非常典型的函數(shù)式編程特性。大量的前端框架也標榜自己使用了函數(shù)式編程的特性,好像一旦跟函數(shù)式編程沾邊,就很高大上一樣,而且還有一些專門針對函數(shù)式編程的框架和庫,比如:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS等。函數(shù)式編程變得越來越流行,掌握這種編程范式對書寫高質(zhì)量和易于維護的代碼都大有好處,所以我們有必要掌握它。
2.什么是函數(shù)式編程?
2.1維基百科定義:
函數(shù)式編程(英語:functional programming),又稱泛函編程,是一種編程范式,它將電腦運算視為數(shù)學上的函數(shù)計算,避免了狀態(tài)的變化和數(shù)據(jù)的可變。
2.2 簡單理解函數(shù)式編程的思想
在函數(shù)式編程之前,我們一直接觸的是面向?qū)ο蟮木幊趟枷?,兩者的區(qū)別:
-
面向?qū)ο?/code>的世界里我們是把事物抽象成類和對象,然后通過封裝、繼承和多態(tài)來演示他們之間的關系。 -
函數(shù)式的世界里把世界抽象成事物和事物之間的關系,用這種方式實現(xiàn)世界的模型。
面向?qū)ο蟮木幊谈腥饲槲兑恍└鐣?。比如你想要買一輛汽車,想要托人幫忙砍價。恰好你同學的朋友的大表哥的二嫂子在4s店工作。你就需要:
import "同學"
import "朋友"
import "大表哥"
import "二嫂子"
然后再調(diào)用二嫂子.砍價();
函數(shù)式編程則更“冰冷”一些,像是工廠里的流水線不停的運轉(zhuǎn),只要你放入材料就可以在出口處拿到產(chǎn)品。而且它對外部環(huán)境沒有依賴,只需要將流水線搬走就可以在任何地方生產(chǎn)。不需要找同學的朋友的大表哥的二嫂子來幫忙了。
相對于面向?qū)ο缶幊蹋∣bject-oriented programming)關注的是數(shù)據(jù)而言,函數(shù)式編程關注的則是動作,其是一種過程抽象的思維,就是對當前的動作去進行抽象。
比如說我要計算一個數(shù) 加上 4 再乘以 4 的值,按照正常寫代碼的邏輯,我們可能會這么去實現(xiàn)
function calculate(x){
return (x + 4) * 4;
}
console.log(calculate(1)) // 20
這是沒有任何問題的,我們在平時開發(fā)的過程中會經(jīng)常將需要重復的操作封裝成函數(shù)以便在不同的地方能夠調(diào)用。但從函數(shù)式編程的思維來看的話,我們關注的則是這一系列操作的動作,先「加上 4」再「乘以 4」。
如何封裝函數(shù)才是最佳實踐呢?如何封裝才能使函數(shù)更加通用,使用起來讓人感覺更加舒服呢?函數(shù)式編程中的合成(compose)或許能給我們一些啟發(fā)。之后會講到。
函數(shù)式編程具有兩個基本特征
- 函數(shù)是純函數(shù)
- 函數(shù)是第一等公民
函數(shù)式編程具有兩個最基本的運算
- 柯里化(Currying)
- 合成(compose)
3.函數(shù)是純函數(shù)
3.1 什么是純函數(shù)
當我們想要理解函數(shù)式編程時,需要知道的第一個基本概念是純函數(shù),但純函數(shù)又是什么鬼?
咱們怎么知道一個函數(shù)是否是純函數(shù)?這里有一個非常嚴格的定義:
- 1.此函數(shù)在相同的輸入值時,總是產(chǎn)生相同的輸出。(引用透明)
- 2.它不會引起任何
副作用。(不可變性)
首先來看第一點:函數(shù)的返回結(jié)果只依賴于它的參數(shù)
let xs = [1,2,3,4,5]
// 純函數(shù)
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
xs.slice(0,3) //[1,2,3]
// 不純函數(shù)
xs.splice(0,3) //[1,2,3]
xs.splice(0,3) //[4,5]
xs.splice(0,3) //[]
將一個函數(shù)反復執(zhí)行,其中的值已經(jīng)被改變了,從而影響后面的函數(shù)操作。一個純函數(shù)是無論什么輸入,都只對應輸出一個唯一值。
這就是純函數(shù)的第一個條件: 函數(shù)的返回結(jié)果只依賴于它的參數(shù)
接下來解釋第2點:函數(shù)執(zhí)行中沒有副作用
副作用指的是: 在計算結(jié)果的過程中,系統(tǒng)狀態(tài)的一種變化, 或者與外部事件進行觀察的交互。(通俗的說就是改變函數(shù)外面的變量或者計算依賴外部的變量) 再看一個例子:
var min = 21;
// 不純函數(shù)
var ckeck = function(age){
return age >= min;
}
在上面的代碼中, 由于我們定義的變量在我們的函數(shù)作用域之外,導致這個函數(shù)稱為“不純”函數(shù)
// 純函數(shù)
var check = function(age){
var min = 21;
return age >= min;
}
上面的代碼,我們只計算了作用域內(nèi)的局部變量, 沒有任何作用域外部的變量被改變, 因此這個函數(shù)是 “純函數(shù)”
除了修改外部的變量, 一個函數(shù)在執(zhí)行過程中還有很多方式產(chǎn)生外部可觀察的變化,比如說調(diào)用 DOM API修改頁面, 或者你發(fā)送了Ajax請求, 還有調(diào)用window.reload刷新瀏覽器, 甚至是console.log往控制臺打印數(shù)據(jù)也是副作用。
3.2 什么是函數(shù)副作用?
函數(shù)副作用是指當調(diào)用函數(shù)時,除了返回函數(shù)值之外,還對主調(diào)用函數(shù)產(chǎn)生附加的影響。副作用的函數(shù)不僅僅只是返回了一個值,而且還做了其他的事情,比如:
- 1、修改了一個變量
- 2、直接修改數(shù)據(jù)結(jié)構(gòu)
- 3、設置一個對象的成員
- 4、拋出一個異?;蛞砸粋€錯誤終止
- 5、打印到終端或讀取用戶輸入
- 6、讀取或?qū)懭胍粋€文件
- 7、在屏幕上畫圖
函數(shù)副作用會給程序設計帶來不必要的麻煩,給程序帶來十分難以查找的錯誤,并且降低程序的可讀性,嚴格的函數(shù)式語言要求函數(shù)必須無副作用。
3.3 使用純函數(shù)的優(yōu)點
為什么要煞費苦心地構(gòu)建純函數(shù)?因為純函數(shù)非?!翱孔V”,執(zhí)行一個純函數(shù)你不用擔心它會干什么壞事,它不會產(chǎn)生不可預料的行為,也不會對外部產(chǎn)生影響。不管何時何地,你給它什么它就會乖乖地吐出什么。如果你的應用程序大多數(shù)函數(shù)都是由純函數(shù)組成,那么你的程序測試、調(diào)試起來會非常方便。
3.3.1 ?? 可測試性
綜上所述,這個就很簡單了,我們不需要關心其它外部的信息,只需要給函數(shù)特定的輸入,再斷言其輸出就好了
一個簡單的例子是接收一組數(shù)字,并對每個數(shù)進行加 1 這種沙雕的操作。
let list = [1, 2, 3, 4, 5];
const incrementNumbers = (list) => list.map(number => number + 1);
接收numbers數(shù)組,使用map遞增每個數(shù)字,并返回一個新的遞增數(shù)字列表。
incrementNumbers(list); // [2, 3, 4, 5, 6]
對于輸入[1,2,3,4,5],預期輸出是[2,3,4,5,6]。
3.3.2 ?? 可緩存性
純函數(shù)可以根據(jù)輸入來做緩存
// 下面的代碼我們可以發(fā)現(xiàn)相同的輸入,再第二次調(diào)用的時候都是直接取的緩存
let squareNumber = memoize((x) => { return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 從緩存中讀取輸入值為 4 的結(jié)果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 從緩存中讀取輸入值為 5 的結(jié)果
//=> 25
怎么實現(xiàn)呢? 我們接著看下面的代碼
const memoize = (f) => {
// 由于使用了閉包,所以函數(shù)執(zhí)行完后cache不會立刻被回收
const cache = {};
return () => {
var arg_str = JSON.stringify(arguments);
// 關鍵就在這里,我們利用純函數(shù)相同輸入相同輸出的邏輯,在這里利用cache做一個簡單的緩存,當這個參數(shù)之前使用過時,我們立即返回結(jié)果就行
cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
return cache[arg_str];
};
};
3.3.3 ?? 可移植性
由于純函數(shù)是自給自足的,它需要的東西都在輸入?yún)?shù)中已經(jīng)聲明,所以它可以任意移植到任何地方。
并且純函數(shù)對于自己的依賴是 誠實的,這一點你看它的 形參 就知道啦~正所謂 形參起的好,注釋不用搞(雙押!)純函數(shù)就是這么個正直的小可愛
// 不純的
var signUp = function(attrs){
// 一些副作用操作
var user = saveUser(attrs);
welcomeUser(user);
}
// 純的
// 相同的輸入總是返回一個函數(shù)
var signUp = function(Db, Email, attrs){
return function(){
//一些副作用操作
var user = saveUser(Db, attrs);
welcomeUser(Email, user);
}
}
3.4 純函數(shù)的特點
3.4.1?? 不可變性
不可變性是純函數(shù)的一個特點: 當數(shù)據(jù)為不可變數(shù)據(jù)(純函數(shù)希望數(shù)據(jù)為不可變數(shù)據(jù))時,他的狀態(tài)在純函數(shù)創(chuàng)建之后不能更改。
作為前端開發(fā)者,你會感受到JS中對象(Object)這個概念的強大。我們說“JS中一切皆對象”。最核心的特性,例如從String,到數(shù)組,再到瀏覽器的APIs,“對象”這個概念無處不在。
JS中的對象是那么美妙:我們可以隨意復制他們,改變并刪除他們的某項屬性等。但是要記住一句話:
“伴隨著特權(quán),隨之而來的是更大的責任?!?br> (With great power comes great responsibility)
這樣所以修改對象,隨之而來的就是副作用。這與函數(shù)式編程的思想是相違背的,所以函數(shù)式編程提出了不可變數(shù)據(jù)的概念。
不可變數(shù)據(jù)是指那些創(chuàng)建后不能更改的數(shù)據(jù)。與許多其他語言一樣,JavaScript 里有一些基本類型(String,Number 等)從本質(zhì)上是不可變的,但是對象就是在任意的地方可變,比如:
let arr = [1, 2, 3, 4, 5];
arr.splice(1, 1); // 返回[2];
console.log(arr); // [1, 3, 4, 5];
這是我們常用的“刪除數(shù)組某一項”的操作。好吧,他一點問題也沒有。
問題其實出現(xiàn)在“濫用”可變性上,這樣會給你的程序帶來“副作用”。先不必關心什么是“副作用”,他又是一個函數(shù)式編程的概念。
我們先來看一下代碼實例:
const student1 = {
school: 'Baidu',
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
const newStudent = student;
newStudent.name = newName;
newStudent.birthdate = newBday;
return newStudent;
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
我們發(fā)現(xiàn),盡管創(chuàng)建了一個新的對象student2,但是老的對象student1也被改動了。這是因為JS對象中的賦值是“引用賦值”,即在賦值過程中,傳遞的是在內(nèi)存中的引用(memory reference)。具體說就是“棧存儲”和“堆存儲”的問題。
不可變數(shù)據(jù)的強大和實現(xiàn)
我們說的“不可變”,其實是指保持一個對象狀態(tài)不變。這樣做的好處是使得開發(fā)更加簡單,可回溯,測試友好,減少了任何可能的副作用。
那么我們避免副作用,創(chuàng)建不可變數(shù)據(jù)的主要實現(xiàn)思路就是:一次更新過程中,不應該改變原有對象,只需要新創(chuàng)建一個對象用來承載新的數(shù)據(jù)狀態(tài)。
我們使用純函數(shù)(pure functions)來實現(xiàn)不可變性。純函數(shù)指無副作用的函數(shù)。
那么,具體怎么構(gòu)造一個純函數(shù)呢?我們可以看一下代碼實現(xiàn),我對上例進行改造:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
}
const changeStudent = (student, newName, newBday) => {
return {
...student, // 使用解構(gòu)
name: newName, // 覆蓋name屬性
birthdate: newBday // 覆蓋birthdate屬性
}
}
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15"}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10"}
需要注意的是,我使用了ES6中的解構(gòu)(destructuring)賦值。
這樣,我們達到了想要的效果:根據(jù)參數(shù),產(chǎn)生了一個新對象,并正確賦值,最重要的就是并沒有改變原對象。也可以使用Object.assign來實現(xiàn)。
同樣,如果是處理數(shù)組相關的內(nèi)容,我們可以使用:.map, .filter或者.reduce去達成目標。這些APIs的共同特點就是不會改變原數(shù)組,而是產(chǎn)生并返回一個新數(shù)組。這和純函數(shù)的思想不謀而合。
但是,再說回來,使用Object.assign請務必注意以下幾點:
1)他的復制,是將所有可枚舉屬性,復制到目標對象。換句話說,不可枚舉屬性是無法完成復制的。
2)對象中如果包含undefined和null類型內(nèi)容,會報錯。
3)最重要的一點:Object.assign方法實行的是淺拷貝,而不是深拷貝。
第三點很重要,也就是說,如果源對象某個屬性的值是對象,那么目標對象拷貝得到的是這個屬性對象的引用。這也就意味著,當對象存在嵌套時,還是有問題的。比如下面代碼:
const student1 = {
school: "Baidu",
name: 'HOU Ce',
birthdate: '1995-12-15',
friends: {
friend1: 'ZHAO Wenlin',
friend2: 'CHENG Wen'
}
}
const changeStudent = (student, newName, newBday, friends) => Object.assign({}, student, {name: newName, birthdate: newBday})
const student2 = changeStudent(student1, 'YAN Haijing', '1990-11-10');
// both students will have the name properties
console.log(student1, student2);
// Object {school: "Baidu", name: "HOU Ce", birthdate: "1995-12-15", friends: Object}
// Object {school: "Baidu", name: "YAN Haijing", birthdate: "1990-11-10", friends: Object}
student2.friends.friend1 = 'MA xiao';
console.log(student1.friends.friend1); // "MA xiao"
對student2 friends列表當中的friend1的修改,同時也影響了student1 friends列表當中的friend1。這個時候就需要進行深拷貝了。上面提到的Object.assign就是典型的淺拷貝。如果遇到嵌套很深的結(jié)構(gòu),我們就需要手動遞歸。這樣做呢,又會存在性能上的問題。
比如我自己動手用遞歸實現(xiàn)一個深拷貝,需要考慮循環(huán)引用的“死環(huán)”問題,另外,當使用大規(guī)模數(shù)據(jù)結(jié)構(gòu)時,性能劣勢盡顯無疑。我們熟悉的jquery extends方法,某一版本(最新版本情況我不太了解)的實現(xiàn)是進行了三層拷貝,也沒有達到完備的deep copy。
總之,實現(xiàn)不可變數(shù)據(jù),我們必然要關心性能問題。針對于此,我推薦一款已經(jīng)“大名鼎鼎”的——immutable.js類庫來處理不可變數(shù)據(jù)。
他的實現(xiàn)既保證了不可變性,又保證了性能大限度優(yōu)化。原理很有意思,下面這段話,我摘自camsong前輩的文章:
Immutable實現(xiàn)的原理是Persistent Data Structure(持久化數(shù)據(jù)結(jié)構(gòu)),也就是使用舊數(shù)據(jù)創(chuàng)建新數(shù)據(jù)時,要保證舊數(shù)據(jù)同時可用且不變。
同時為了避免deepCopy把所有節(jié)點都復制一遍帶來的性能損耗,Immutable使用了Structural Sharing(結(jié)構(gòu)共享),即如果對象樹中一個節(jié)點發(fā)生變化,只修改這個節(jié)點和受它影響的父節(jié)點,其它節(jié)點則進行共享。
感興趣的讀者可以深入研究下,這是很有意思的。如果有需要,我也愿意再寫一篇immutable.js源碼分析。
3.4.2 ??引用透明
如果一個函數(shù)對于相同的輸入始終產(chǎn)生相同的結(jié)果,那么我們就說它是引用透明。
接著實現(xiàn)一個square 函數(shù):
const square = (n) => n * n;
給定相同的輸入,這個純函數(shù)總是有相同的輸出。
square(2); // 4
square(2); // 4
square(2); // 4
// ...
將2作為square函數(shù)的參數(shù)傳遞始終會返回4。這樣咱們可以把square(2)換成4,我們的函數(shù)就是引用透明的。
基本上,如果一個函數(shù)對于相同的輸入始終產(chǎn)生相同的結(jié)果,那么它可以看作透明的。
有了這個概念,咱們可以做的一件很酷的事情就是記住這個函數(shù)。假設有這樣的函數(shù)
const sum = (a, b) => a + b;
用這些參數(shù)來調(diào)用它
sum(3, sum(5, 8));
sum(5, 8) 總等于13,所以可以做些騷操作:
sum(3, 13);
這個表達式總是得到16,咱們可以用一個數(shù)值常數(shù)替換整個表達式,并把它記下來。
3.4.3 ?? 聲明式編程
先統(tǒng)一一下概念,我們有兩種編程方式:命令式和聲明式。
我們可以像下面這樣定義它們之間的不同:
- 命令式編程:命令“機器”如何去做事情(how),這樣不管你想要的是什么(what),它都會按照你的命令實現(xiàn)。
聲明式編程:告訴“機器”你想要的是什么(what),讓機器想出如何去做(how)。
聲明式編程和命令式編程的代碼例子
舉個簡單的例子,假設我們想讓一個數(shù)組里的數(shù)值翻倍。
我們用命令式編程風格實現(xiàn),像下面這樣:
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push (newNumber)
}
console.log (doubled) //=> [2,4,6,8,10]
我們直接遍歷整個數(shù)組,取出每個元素,乘以二,然后把翻倍后的值放入新數(shù)組,每次都要操作這個雙倍數(shù)組,直到計算完所有元素。
而使用聲明式編程方法,我們可以用 Array.map 函數(shù),像下面這樣:
var numbers = [1,2,3,4,5]
var doubled = numbers.map (function (n) {
return n * 2
})
console.log (doubled) //=> [2,4,6,8,10]
map利用當前的數(shù)組創(chuàng)建了一個新數(shù)組,新數(shù)組里的每個元素都是經(jīng)過了傳入map的函數(shù)(這里是function (n) { return n*2 })的處理。
用上面的定義方式來分析:
- 命令式:for循環(huán)遍歷數(shù)組,關注運行原理,強調(diào)How
- 聲明式:Array.map遍歷數(shù)組,關注輸出結(jié)果,強調(diào)What
在一些具有函數(shù)式編程特征的語言里,對于 list 數(shù)據(jù)類型的操作,還有一些其他常用的聲明式的函數(shù)方法。例如,求一個list里所有值的和,命令式編程會這樣做:
var numbers = [1,2,3,4,5]
var total = 0 for(var i = 0; i < numbers.length; i++) {
total += numbers[i]
}
console.log (total) //=> 15
而在聲明式編程方式里,我們使用reduce函數(shù):
var numbers = [1,2,3,4,5]
var total = numbers.reduce (function (sum, n) {
return sum + n
});
console.log (total) //=> 15
reduce函數(shù)利用傳入的函數(shù)把一個list運算成一個值。它以這個函數(shù)為參數(shù),數(shù)組里的每個元素都要經(jīng)過它的處理。每一次調(diào)用,第一個參數(shù)(這里是sum)都是這個函數(shù)處理前一個值時返回的結(jié)果,而第二個參數(shù)(n)就是當前元素。這樣下來,每此處理的新元素都會合計到sum中,最終我們得到的是整個數(shù)組的和。
同樣,reduce函數(shù)歸納抽離了我們?nèi)绾伪闅v數(shù)組和狀態(tài)管理部分的實現(xiàn),提供給我們一個通用的方式來把一個list合并成一個值。我們需要做的只是指明我們想要的是什么。
4.函數(shù)是第一等公民
第一等公民是指函數(shù)跟其它的數(shù)據(jù)類型一樣處于平等地位,可以賦值給其他變量,可以作為參數(shù)傳入另一個函數(shù),也可以作為別的函數(shù)的返回值。
// 賦值
var a = function fn1() { }
// 函數(shù)作為參數(shù)
function fn2(fn) {
fn()
}
// 函數(shù)作為返回值
function fn3() {
return function() {}
}
下面這些術語都是圍繞這一特性的應用:
4.1 閉包
閉包是js開發(fā)慣用的技巧,什么是閉包?閉包就是一個函數(shù),這個函數(shù)能夠訪問其他函數(shù)的作用域中的變量
在函數(shù)式編程的過程中。函數(shù)內(nèi)定義了局部變量并且返回可緩存的函數(shù). 變量在返回的函數(shù)內(nèi)也是可被訪問的, 此處創(chuàng)建了一個閉包
// test1 是普通函數(shù)
function test1() {
var a = 1;
// test2 是內(nèi)部函數(shù)
// 它引用了 test1 作用域中的變量 a
// 因此它是一個閉包
return function test2() {
return a + =1;
}
}
var result = test1();
console.log(result()); // 2
console.log(result()); // 3
console.log(result()); // 4
變量a 被一直保存在內(nèi)存中,沒有被垃圾回收機制回收掉。函數(shù)式編程中兩個最基本的運算:柯里化和合成都使用到了閉包
關于閉包的詳細解釋,請看: 徹底搞懂JS閉包各種坑
4.2 高階函數(shù)
高階函數(shù):以函數(shù)作為參數(shù)的函數(shù),結(jié)果return一個函數(shù)。
上面關于高階函數(shù)的概念其實是錯誤的.高階函數(shù)只要滿足參數(shù)或返回值為函數(shù)就可以成為高階函數(shù),而非一定要同時滿足才成立)
簡單的例子:
function add(a,b,fn){
return fn(a)+fn(b);
}
var fn=function (a){
return a*a;
}
add(2,3,fn); //13
為什么要使用高階函數(shù)(使用高階函數(shù)的意義)
- 高階函數(shù)是用來抽象通用問題的
- 抽象可以幫我們屏蔽細節(jié),只需要關注我們的目標
- 使代碼更簡潔
舉例:比如我們現(xiàn)在需要去遍歷一個數(shù)組,按照面向過程的方式時我們需要使用一個for循環(huán),定義一個循環(huán)變量,判斷循環(huán)條件等操作;如果此時我們使用高階函數(shù)對遍歷這個步驟進行抽象,如上邊實現(xiàn)的forEach函數(shù),我們此時只需要知道forEach內(nèi)部幫我們實現(xiàn)了循環(huán),然后傳遞數(shù)據(jù)給forEach。(前邊的filter函數(shù)也是如此)
編寫一個自己的高階函數(shù)
當我們玩了很多ES6自帶的高階函數(shù)后,就可以升級到自己寫高階函數(shù)的階段了,比如說用函數(shù)式的方式寫一個節(jié)流函數(shù),
節(jié)流函數(shù)說白了,就是一個控制事件觸發(fā)頻率的函數(shù),以前可以一秒內(nèi),無限次觸發(fā),現(xiàn)在限制成500毫秒觸發(fā)一次
function throttle(fn, wait=500) {
if (typeof fn != "function") {
// 必須傳入函數(shù)
throw new TypeError("Expected a function")
}
// 定時器
let timer,
// 是否是第一次調(diào)用
firstTime = true;
// 這里不能用箭頭函數(shù),是為了綁定上下文
return function (...args) {
// 第一次
if (firstTime) {
firstTime = false;
fn.apply(this,args);
}
if (timer) {
return;
}else {
timer = setTimeout(() => {
clearTimeout(timer);
timer = null;
fn.apply(this, args);
},wait)
}
}
}
// 單獨使用,限制快速連續(xù)不停的點擊,按鈕只會有規(guī)律的每500ms點擊有效
button.addEventListener('click', throttle(() => {
console.log('hhh')
}))
常用的高階函數(shù)
- forEach
- map
- filter
- every
- some
- find/findIndex
- reduce
- sort
......
以及現(xiàn)在常用的redux中的connect方法也是高階函數(shù)。
拿map展示下
var pow = function square(x) {
return x * x;
};
var array = [1, 2, 3, 4, 5, 6, 7, 8];
var newArr = array.map(pow); //直接傳入一個函數(shù)方法
console.log(newArr); // [1, 4, 9, 16, 25, 36, 49, 64]
console.log(array); // [1, 2, 3, 4, 5, 6, 7, 8]
map實現(xiàn)
var arr = [1,2,3];
var fn = function(item,index,arr){
console.log(index);
console.log(arr);
return item*2;
};
Array.prototype.maps = function(fn) {
let newArr = [];
for (let index = 0; index < this.length; index++) {
newArr.push(fn.call(this,this[index],index,this));
}
return newArr;
}
var result = arr.maps(fn);
4.3 函數(shù)柯里化(Currying)
函數(shù)柯里化是函數(shù)式編程中高階函數(shù)的一個重要用法
柯里化是指這樣一個函數(shù)(假設叫做createCurry),他接收函數(shù)A作為參數(shù),運行后能夠返回一個新的函數(shù)。并且這個新的函數(shù)能夠處理函數(shù)A的剩余參數(shù)。
這樣的定義不太好理解,我們可以通過下面的例子配合解釋。
有一個接收三個參數(shù)的函數(shù)A。
function A(a, b, c) {
// do something
}
假如,我們有一個已經(jīng)封裝好了的柯里化通用函數(shù)createCurry。他接收bar作為參數(shù),能夠?qū)轉(zhuǎn)化為柯里化函數(shù),返回結(jié)果就是這個被轉(zhuǎn)化之后的函數(shù)。
var _A = createCurry(A);
那么_A作為createCurry運行的返回函數(shù),他能夠處理A的剩余參數(shù)。因此下面的運行結(jié)果都是等價的。
_A(1, 2, 3);
_A(1, 2)(3);
_A(1)(2, 3);
_A(1)(2)(3);
A(1, 2, 3);
函數(shù)A被createCurry轉(zhuǎn)化之后得到柯里化函數(shù)_A,_A能夠處理A的所有剩余參數(shù)。因此柯里化也被稱為部分求值。
在簡單的場景下,可以不用借助柯里化通用式來轉(zhuǎn)化得到柯里化函數(shù),我們憑借眼力自己封裝。
例如有一個簡單的加法函數(shù),他能夠?qū)⒆陨淼娜齻€參數(shù)加起來并返回計算結(jié)果。
function add(a, b, c) {
return a + b + c;
}
那么add函數(shù)的柯里化函數(shù)_add則可以如下:
function _add(a) {
return function(b) {
return function(c) {
return a + b + c;
}
}
}
下面的運算方式是等價的。
add(1, 2, 3);
_add(1)(2)(3);
當然,靠眼力封裝的柯里化函數(shù)自由度偏低,柯里化通用式具備更加強大的能力。因此我們需要知道如何去封裝這樣一個柯里化的通用式。
首先通過_add可以看出,柯里化函數(shù)的運行過程其實是一個參數(shù)的收集過程,我們將每一次傳入的參數(shù)收集起來,并在最里層里面處理。在實現(xiàn)createCurry時,可以借助這個思路來進行封裝。
封裝如下:
function add() {
// 類對象轉(zhuǎn)換成數(shù)組
var _args = [...arguments];
// 在內(nèi)部聲明一個函數(shù),利用閉包的特性保存_args并收集所有的參數(shù)值
var adder = function() {
// [].push.apply(_args, [].slice.call(arguments));
_args.push(...arguments);
return adder;
};
// 利用隱式轉(zhuǎn)換的特性,當最后執(zhí)行時隱式轉(zhuǎn)換,并計算最終的值返回
adder.toString = function() {
return _args.reduce(function(a, b) {
return a + b;
});
}
return adder;
}
var a = add(1)(2)(3)(4); // f 10
var b = add(1, 2, 3, 4); // f 10
var c = add(1, 2)(3, 4); // f 10
var d = add(1, 2, 3)(4); // f 10
// 可以利用隱式轉(zhuǎn)換的特性參與計算
console.log(a + 10); // 20
console.log(b + 20); // 30
console.log(c + 30); // 40
console.log(d + 40); // 50
// 也可以繼續(xù)傳入?yún)?shù),得到的結(jié)果再次利用隱式轉(zhuǎn)換參與計算
console.log(a(10) + 100); // 120
console.log(b(10) + 100); // 120
console.log(c(10) + 100); // 120
console.log(d(10) + 100); // 120
4.4 函數(shù)合成(compose)
函數(shù)合成指的是將代表各個動作的多個函數(shù)合并成一個函數(shù)。
上面講到,函數(shù)式編程是對過程的抽象,關注的是動作。以上面計算的例子為例,我們關注的是它的動作,先「加上 4」再「乘以 4」。那么我們的代碼實現(xiàn)如下:
function add4(x) {
return x + 4
}
function multiply4(x) {
return x * 4
}
console.log(multiply4(add4(1))) // 20
根據(jù)函數(shù)合成的定義,我們能夠?qū)⑸鲜龃韮蓚€動作的兩個函數(shù)的合成一個函數(shù)。我們將合成的動作抽象為一個函數(shù) compose,這里可以比較容易地知道,函數(shù) compose 的代碼如下:
function compose(f,g) {
return function(x) {
return f(g(x));
};
}
所以我們可以通過如下的方式得到合成函數(shù)
var calculate=compose(multiply4,add4); //執(zhí)行動作的順序是從右往左
console.log(calculate(1)) // 20
可以看到,只要往 compose 函數(shù)中傳入代表各個動作的函數(shù),我們便能得到最終的合成函數(shù)。但上述 compose 函數(shù)的局限性是只能夠合成兩個函數(shù),如果需要合成的函數(shù)不止兩個呢,所以我們需要一個通用的 compose 函數(shù)。
這里我直接給出通用 compose 函數(shù)的代碼:
function compose(...args) {
return function(x) {
var composeFun = args.reduceRight(function(funLeft, funRight) {
console.log(funLeft);
return funRight(funLeft)
}, x);
return composeFun;
}
}
讓我們來實踐下上述通用的 compose 函數(shù)~
function addHello(str){
return 'hello '+str;
}
function toUpperCase(str) {
return str.toUpperCase();
}
function reverse(str){
return str.split('').reverse().join('');
}
var composeFn=compose(reverse,toUpperCase,addHello);
console.log(composeFn('ttsy')); // YSTT OLLEH
上述過程有三個動作,「hello」、「轉(zhuǎn)換大寫」、「反轉(zhuǎn)」,可以看到通過 compose 將上述三個動作代表的函數(shù)合并成了一個,最終輸出了正確的結(jié)果。
結(jié)合使用
嗯,到了這里,已經(jīng)初步了解了函數(shù)式編程的概念了,那么我們怎么使用函數(shù)式編程的方式寫代碼呢,舉個例子:
// 偽代碼,思路
// 比如說,我們請求后臺拿到了一個數(shù)據(jù),然后我們需要篩選幾次這個數(shù)據(jù), 取出里面的一部分,并且排序
// 數(shù)據(jù)
const res = {
status: 200,
data: [
{
id: xxx,
name: xxx,
time: xxx,
content: xxx,
created: xxx
},
...
]
}
// 封裝的請求函數(shù)
const http = xxx;
// '傳統(tǒng)寫法是這樣的'
http.post
.then(res => 拿到數(shù)據(jù))
.then(res => 做出篩選)
.then(res => 做出篩選)
.then(res => 取出一部分)
.then(res => 排序)
// '函數(shù)式編程是這樣的'
// 聲明一個篩選函數(shù)
const a = curry()
// 聲明一個取出函數(shù)
const b = curry()
// 聲明一個排序函數(shù)
const c = curry()
// 組合起來
const shout = compose(c, b, a)
// 使用
shout(http.post)
如何在項目中正式使用函數(shù)式編程
我覺得,想要在項目里面正式使用函數(shù)式編程有這樣幾個步驟:
- 1、先嘗試使用ES6自帶的高階函數(shù)
- 2、熟悉了ES6自帶的高階函數(shù)后,可以自己嘗試寫幾個高階函數(shù)
- 3、在這個過程中,盡量使用純函數(shù)編寫代碼
- 4、對函數(shù)式編程有所了解之后,嘗試使用類似ramda的庫來編寫代碼
- 5、在使用ramda的過程中,可以嘗試研究它的源代碼
- 6、嘗試編寫自己的庫,柯里化函數(shù),組合函數(shù)等
當然了,這個只是我自己的理解,我在實際項目中也沒有完全的使用函數(shù)式編程開發(fā),我的開發(fā)原則是:
不要為了函數(shù)式而選擇函數(shù)式編程。如果函數(shù)式編程能夠幫助你,能夠提升項目的效率,質(zhì)量,可以使用;如果不能,那么不用;如果對函數(shù)式編程還不太熟,比如我這樣的,偶爾使用
擴展
函數(shù)式編程是在范疇論的基礎上發(fā)展而來的,而關于函數(shù)式編程和范疇論的關系,阮一峰大佬給出了一個很好的說明,在這里復制粘貼下他的文章
本質(zhì)上,函數(shù)式編程只是范疇論的運算方法,跟數(shù)理邏輯、微積分、行
列式是同一類東西,都是數(shù)學方法,只是碰巧它能用來寫程序
所以,你明白了嗎,為什么函數(shù)式編程要求函數(shù)必須是純的,不能有副作用?因為它是一種數(shù)學運算,原始目的就是求值,不做其他事情,否則就無法滿足函數(shù)運算法則了。