為什么要去學習函數式編程

函數式編程比較復雜比較枯燥,但是為了了解react和redux,如果沒有函數式編程的理論鋪墊,很難學好他們。
函數式編程在js當中是一個比較抽象的概念,大家在以前可能聽說過函數式編程,但是可能并沒有系統(tǒng)的去了解過他們。
函數式編程和面向對象編程一樣,是一套編程范式,你可以根據函數式編程的理論為你的代碼設計這個過程。只不過但是函數式編程要求相對比較高一些。
為什么要去學習函數式編程
函數式編程其實相對于計算機的歷史而言是一個非常古老的概念,甚至早已第一臺計算機的誕生。他的演算并非設計在計算機上執(zhí)行,而是在20世紀三十年代引入的一套用于研究函數定義,函數應用和遞歸的形式系統(tǒng)。
也就是說函數式編程已經是一個很老的概念了,那為什么我們還要學習他,其實函數式編程以前和前端沒有任何關系,也并不流行。只是因為react和redux將它帶火了。有了高階函數,那么高階函數就是函數式編程的一部分,所以才將函數式編程帶火了。
函數式編程主要是用于研究函數的定義,函數的應用和遞歸的而這樣一個形式的系統(tǒng)。
注意,函數式編程不是用函數來編程,也不是傳統(tǒng)的面向過程編程,主旨在于將復雜的函數復合成簡單的函數,運算過程盡量寫成一系列嵌套的函數調用。大家注意區(qū)分用函數編程和函數式編程是不同的。
react的高階組件,使用了高階函數來實現,高階函數就是函數式編程的一個特性,我們后面會學到。雖然react當中使用了一些函數式編程的特性,但它并不是純函數式的。
另外react的一些生態(tài),比如redux,它使用了函數式編程的一些思想,所以我們想要更好的學習react和redux的話,我們需要了解函數式編程。
我們都知道vue3對vue2做了很大的重構,而且越來越偏向函數式,我們在使用vue3的一些api的時候可以感受到,在vue2的源碼中也大量的時候到了高階函數,這些流行框架都在趨向于函數式編程,我們甚至可以說你可以不學習這些框架,但是你不能不了解函數式編程。
因為這些才是永遠不變的內容。
很多同學再學習js之前可能都了解過面向對象的語言,比如說Java,C#以及C++等等,所以在學習js的時候,我們也都是從面向對象開始學習的,我們會通過學習原型,原型鏈以及模擬實現繼承的機制來實現面向對象的一些特性。
我們在學習的過程中還會遇到this的各種各樣問題,如果我們用到函數式編程的時候,我們就可以拋棄掉this。
是用函數式編程有很多的好處,比如說打包的時候可 以更好的利用tree-shaking來過濾無用的代碼。
使用函數式編程還可以方便測試,方便并行處理,這些都是由函數式編程的特性來決定的。
還有很多庫可以幫助我們進行函數式開發(fā),比如說lodash,underscore,ramda。
這就是為什么要學習函數式編程。
函數式編程的概念
函數式編程是范疇輪的數學分支,是一門很復雜的數學,認為世界上所有的概念體系都可以抽象出一個范疇。范疇可以理解為群體的概念,比如一個班級中的同學,就可以理解為一個范疇。
只要彼此之間存在某種關系概念,事物,對象等等,都構成范疇,任何事物只要找出他們之間的關系,就可以被定義。
比如說教室中上課的人,可以彼此都不認識,但是大家的關系是同學,所以就是一個范疇。
關系一般用箭頭來表示,正式的名稱叫做 態(tài)射 。范疇輪認為,同一個范疇的所有成員,就是不同狀態(tài)的變形。
通過態(tài)射一個成員就可以變形成另一個成員。簡單來說就是每個成員之間都是有關系的。
函數式編程英文的叫法是Functional Programming 縮寫是FP。函數式編程是一種編程范式,我們可以認為他是一種編程的風格,他和面向對象是并列的關系。函數式編程我們可以認為是一種思維的模式,我們常聽說的編程范式,還有面向過程變成和面向對象編程。
函數式編程的思維方式,是把現實世界中的事物,和事物之間的聯系,抽象到程序世界中。
我們首先來解釋一下程序的本質,就是根據輸入然后根據某種運算獲得相應的輸出,程序在開發(fā)的過程中會涉及到很多說如和輸出的函數,函數式編程就是對這些運算過程進行抽象。
假設我們有個輸入x,可以通過一個映射關系變成y,那這個映射關系就是函數式編程中的函數。
關于函數式編程我們要注意的是,函數式編程中的函數,不是程序中的函數或者方法,不是說我們在編程過程中使用到了函數或者方法就是函數式編程,函數式編程中的函數,值得其實是數學中的函數,數學中的函數是用來描述映射關系的,例如 y = sin(x) 這個函數,sin是用來描述x和y的關系。
當x=1時y的值也就確定了,也就是說當x的值確定了y的值一定也是確定的。
在函數式編程中我們要求相同的輸入始終要得到相同的輸出,這是純函數的概念。
函數式編程就是對運算過程的抽象,下面我們用一段代碼來體會一下函數式編程。
比如我們要計算兩個數的和,并且打印這個結果,我們一般會定義兩個變量num1和num2,然后將這個兩個變量想加,最后打印想加的結果。
let num1 = 2;let num2 = 3;let num = num1 + num2;console.log(sum)
那這是非函數式的,如果使用函數式的思想應該像下面這樣,首先我們要對運算過程抽象add函數,這個函數接收兩個參數n1和n2,當這個函數執(zhí)行之后會把結果返回。也就是說,函數式編程中的函數一定要有輸入和輸出。
function add (n1, n2) {return n1 + n2;}let sum = add(2, 3);console.log(sum);
可以看到,當我們使用函數式編程的時候一定會有一些函數,這些函數后續(xù)可以無數次的重用,所以函數式編程的一個好處就是,可以讓代碼進行重用,而且在函數式編程的過程中,抽象出來的函數都是細粒度的,那這些函數將來可以重新去組合成功能更強大的函數。
函數式編程不是說寫幾個函數就是函數式開發(fā),他是用數學的思維方式借助js的語法來進行一些代碼的開發(fā),所以說函數式他是一套數學的規(guī)律。
那這樣他跟我們平常寫代碼有什么區(qū)別呢?用函數式編程的時候我們是不可以用if的,也沒有else,因為數學中不存在if和else,也沒有變量和while,整個都是數學的思維,然后用js的語法來承接。可以使用遞歸,因為遞歸是數學的概念。
函數是一等公民
所謂一等公民,指的是函數與其它數據類型一樣,處于平等地位,可以賦值給其它變量,可以作為參數,也可以作為返回值。
在函數式編程中,變量是不能被修改的,所有的變量只能被賦值一次,所有的值全都靠傳參來解決。
所以簡單來說就是,函數是一等公民,可以賦值給變量,可以當做參數傳遞,可以作為返回值。
在函數式編程中,只能用表達式,不能用語句,因為數學里面沒有語句。
因為變量只能被賦值一次,不能修改變量的值,所以不存在副作用,也不能修改狀態(tài)。
函數之間運行全靠參數傳遞,而且這個參數是不會被修改的,所以引用比較透明。
高階函數
高階函數的定義其實很簡單,就是如果一個函數A可以接收另一個函數B作為參數,那么這種函數A就稱之為高階函數。說簡單一點就是參數列表中包含函數。
函數式編程的思想是對運算過程進行抽象,也就是把運算過程抽象成函數,然后在任何地方都可以去重用這些函數。
抽象可以幫我們屏蔽實現的細節(jié),我們以后在調用這些函數的時候只需要關注我們的目標就可以了,那高階函數就是幫我們抽象這些通用的問題。
我們舉一個簡單的例子,比如說我們想遍歷打印數組中的每一個元素,如果我們使用面向過程編程代碼如下。
可以發(fā)現我們要寫一個循環(huán)來做這樣一件事,我們要關注數組的長度,要控制變量不能大于數組長度,要關心很多東西。
// 面向過程方式let array = [1, 2, 3, 4];for (let i = 0; i < array.length; i++) {console.log(array[i]);}
我們這里Array的forEach函數實現,我們在使用的時候不需要關注循環(huán)的具體實現,也就是不需要關注循環(huán)的細節(jié),也不需要變量去控制,我們只需要知道forEach函數可以幫我們完成循環(huán)就ok了。
// 高階函數let array = [1, 2, 3, 4];array.forEach(item => {console.log(item);})
這里的forEach就是對通用問題的一個抽象,我們可以看到使用forEach要比for循環(huán)簡潔很多,所以我們使用函數式編程還有一個好處就是使代碼更簡潔。
在js中,數組的forEach,map,filter,every,some,find, findIndex, reduce, sort等都是高階函數,因為他們都可以接收一個函數為參數。
閉包
函數和其周圍的狀態(tài)的引用捆綁在一起,可以在另一個作用域中調用這個函數內部的函數并訪問到該函數作用域中的成員。
閉包的概念并不復雜,但是他的定義比較繞,我們通過一段代碼來體會閉包的概念。
首先我們定義一個makeFn的函數,在這個函數中定義一個變量msg,當這個函數調用之后,msg就會被釋放掉。
function makeFn () {let msg = 'Hello';}maknFn();
如果我們在makeFn中返回一個函數,在這個函數中又訪問了msg,那這就是閉包。
和剛剛不一樣的是,當我們調用完makeFn之后他會返回一個函數,接收的fn其實就是接收makeFn返回的函數,也就意味著外部的fn對函數內部的msg存在引用。
所以當我們調用fn的時候,也就是調用了內部函數,會訪問到msg,也就是makeFn中的變量。
function makeFn () {let msg = 'Hello';return function() {console.log(msg);}}const fn = maknFn();fn();
所以閉包就是在另一個作用域(這里是全局),可以調用到一個函數內部的函數(makeFn內部返回的函數),在這個函數中可以訪問到這個函數(makeFn)作用域中的成員。
根據上面的描述,閉包的核心作用就是把我們makeFn中內部成員的作用范圍延長了,正常情況下makeFn執(zhí)行完畢之后msg會被釋放掉,但是這里因為外部還在繼續(xù)引用msg,所以并沒有被釋放。
我們接下來看下下面這個例子, 介紹一下閉包的作用。
這里有一個once函數,他的作用就是控制fn函數只會執(zhí)行一次,那如何控制fn只能執(zhí)行一次呢?這里就需要有一個標記來記錄,這個函數是否被執(zhí)行了,我們這里定義一個局部變量done,默認情況下是false,也就是fn并沒有被執(zhí)行。
在once函數內部返回了一個函數,在這個新返回的函數內部先去判斷done,如果done為false,就把他標記為true,并且返回fn的調用。
當once被執(zhí)行的時候,我們創(chuàng)建一個done,并且返回一個函數。這個函數我們賦值給pay。
當我們調用pay的時候,會訪問到外部的done,判斷done是否為false,如果是將done修改為true,并且執(zhí)行fn。這樣在下一次次調用pay的時候,由于done已經為true了,所以就不會再次執(zhí)行了。
function once(fn) {let done = false;return function() {if (!done) {done = true;return fn.apply(this, arguments);}}}let pay = once(function(money) {console.log(`${money}`);});// 只會執(zhí)行一次。pay(1);pay(2);
閉包的本質就是,函數在執(zhí)行的時候會放到一個執(zhí)行棧上執(zhí)行,當函數執(zhí)行完畢之后會從執(zhí)行棧上移除,但是堆上的作用域成員因為被外部引用不能釋放,因此內部函數依然可以訪問外部函數的成員。

