一個持續更新的github筆記,連接地址:Front-End-Basics,能夠watch,也能夠star。javascript
此篇文章的地址:JavaScript函數式編程入門經典html
正文開始java
f(x) = y
// 一個函數f,以x爲參數,並返回輸出y
複製代碼
關鍵點:git
函數式編程技術主要基於數學函數和它的思想,因此要理解函數式編程,先了解數學函數是有必要的。github
函數是一段能夠經過其名稱被調用的代碼。它能夠接受參數,並返回值。面試
與面向對象編程(Object-oriented programming)和過程式編程(Procedural programming)同樣,函數式編程(Functional programming)也是一種編程範式。咱們可以以此建立僅依賴輸入就能夠完成自身邏輯的函數。這保證了當函數被屢次調用時仍然返回相同的結果(引用透明性)。函數不會改變任何外部環境的變量,這將產生可緩存的,可測試的代碼庫。編程
全部的函數對於相同的輸入都將返回相同的值,函數的這一屬性被稱爲引用透明性(Referential Transparency)數組
// 引用透明的例子,函數identity不管輸入什麼,都會原封不動的返回
var identity = (i) => {return i}
複製代碼
把一個引用透明的函數用於其餘函數調用之間。緩存
sum(4,5) + identity(1)
bash
根據引用透明的定義,咱們能夠把上面的語句換成:
sum(4,5) + 1
該過程被稱爲替換模型(Substitution Model),由於函數的邏輯不依賴其餘全局變量,你能夠直接替換函數的結果,這與它的值是同樣的。因此,這使得併發代碼和緩存成爲可能。
併發代碼: 併發運行的時候,若是依賴了全局數據,要保證數據一致,必須同步,並且必要時須要鎖機制。遵循引用透明的函數只依賴參數的輸入,因此能夠自由的運行。
緩存: 因爲函數會爲給定的輸入返回相同的值,實際上咱們就能緩存它了。好比實現一個計算給定數值的階乘的函數,咱們就能夠把每次階乘的結果緩存下來,下一次直接用,就不用計算了。好比第一次輸入5,結果是120,第二次輸入5,咱們知道結果必然是120,因此就能夠返回已緩存的值,而沒必要再計算一次。
函數式編程主張聲明式編程和編寫抽象的代碼。
// 有一個數組,要遍歷它並把它打印到控制檯
/*命令式*/
var array = [1,2,3]
for(var i = 0; i < array.length; i++)
console(array[i]) // 打印 1,2,3
// 命令式編程中,咱們精確的告訴程序應該「如何」作:獲取數組的長度,經過數組的長度循環數組,在每一次循環中用索引獲取每個數組元素,而後打印出來。
// 可是咱們的任務只是打印出數組的元素。並非要告訴編譯器要如何實現一個遍歷。
/*聲明式*/
var array = [1,2,3]
array.forEach((element) => console.log(element)) // 打印 1,2,3
// 咱們使用了一個處理「如何」作的抽象函數,而後咱們就能只關心作「什麼」了
複製代碼
大多數函數式編程的好處來自於編寫純函數,純函數是對給定的輸入返回相同的輸出的函數,而且純函數不該依賴任何外部變量,也不該改變任何外部變量。
程序做用於數據,數據對於程序的執行很重要。每種編程語言都有數據類型。這些數據類型可以存儲數據並容許程序做用其中。
**當一門語言容許函數做爲任何其餘數據類型使用時,函數被稱爲一等公民。**也就是說函數可被賦值給變量,做爲參數傳遞,也可被其餘函數返回。
函數做爲JavaScript的一種數據類型,因爲函數是相似String的數據類型,因此咱們能把函數存入一個變量,可以做爲函數的參數進行傳遞。因此JavaScript中函數是一等公民。
接受另外一個函數做爲其參數的函數稱爲高階函數(Higher-Order-Function),或者說高階函數是接受函數做爲參數而且/或者返回函數做爲輸出的函數。
通常而言,高階函數一般用於抽象通用的問題,換句話說,高階函數就是定義抽象。
抽象 : 在軟件工程和計算機科學中,抽象是一種管理計算機系統複雜性的技術。 經過創建一我的與系統進行交互的複雜程度,把更復雜的細節抑制在當前水平之下。簡言之,抽象讓咱們專一於預約的目標而無須關心底層的系統概念。
例如:你在編寫一個涉及數值操做的代碼,你不會對底層硬件的數字表現方式究竟是16位仍是32位整數有很深的瞭解,包括這些細節在哪裏屏蔽。由於它們被抽象出來了,只留下了簡單的數字給咱們使用。
// 用forEach抽象出遍歷數組的操做
const forEach = (array,fn) => {
let i;
for(i=0;i<array.length;i++) {
fn(array[i])
}
}
// 用戶不須要理解forEach是如何實現遍歷的,如此問題就被抽象出來了。
//例如,想要打印出數組的每一項
let array = [1,2,3]
forEach(array,(data) => console.log(data))
複製代碼
什麼是閉包?簡言之,**閉包就是一個內部函數。**什麼是內部函數?就是在另外一個函數內部的函數。
閉包的強大之處在於它對做用域鏈(或做用域層級)的訪問。從技術上講,閉包有3個可訪問的做用域。
(1) 在它自身聲明以內聲明的變量
(2) 對全局變量的訪問
(3) 對外部函數變量的訪問(關鍵點)
實例一:假設你再遍歷一個來自服務器的數組,並發現數據錯了。你想調試一下,看看數組裏面究竟包含了什麼。不要用命令式的方法,要用函數式的方法來實現。這裏就須要一個 tap 函數。
const tap = (value) => {
return (fn) => {
typeof fn === 'function' && fn(value)
console.log(value)
}
}
// 沒有調試以前
forEach(array, data => {
console.log(data + data)
})
// 在 forEach 中使用 tap 調試
forEach(array, data => {
tap(data)(() => {
console.log(data + data)
})
})
複製代碼
完成一個簡單的reduce函數
const reduce = (array,fn,initialValue) => {
let accumulator;
if(initialValue != undefined)
accumulator = initialValue
else
accumulator = array[0]
if(initialValue === undefined)
for(let i = 1; i < array.length; i++)
accumulator = fn(accumulator, array[i])
else
for(let value of array)
accumulator = fn(accumulator,value)
return accumulator
}
console.log(reduce([1,2,3], (accumulator,value) => accumulator + value))
// 打印出6
複製代碼
只接受一個參數的函數稱爲一元(unary)函數。
只接受兩個參數的函數稱爲二元(binary)函數。
變參函數是接受可變數量的函數。
柯里化是把一個多參數函數轉換爲一個嵌套的一元函數的過程。
例如
// 一個多參數函數
const add = (x,y) => x + y;
add(2,3)
// 一個嵌套的一元函數
const addCurried = x => y => x + y;
addCurried(2)(3)
// 而後咱們寫一個高階函數,把 add 轉換成 addCurried 的形式。
const curry = (binaryFn) => {
return function (firstArg) {
return function (secondArg) {
return binaryFn(firstArg,secondArg)
}
}
}
let autoCurriedAdd = carry(add)
autoCurriedAdd(2)(3)
複製代碼
上面只是簡單實現了一個二元函數的柯里化,下面咱們要實現一個更多參數的函數的柯里化。
const curry = (fn) => {
if (typeof fn !== 'function') {
throw Error('No function provided')
}
return function curriedFn (...args) {
// 判斷當前接受的參數是否是小於進行柯里化的函數的參數個數
if(args.length < fn.length) {
// 若是小於的話就返回一個函數再去接收剩下的參數
return function (...argsOther) {
return curriedFn.apply(null, args.concat(argsOther))
}
}else {
return fn.apply(null,args)
}
}
}
const multiply = (x,y,z) => x * y * z;
console.log(curry(multiply)(2)(3)(4))
複製代碼
柯里化的應用實例:從數組中找出含有數字的元素
let match = curry(function (expr,str) {
return str.match(expr)
})
let hasNumber = match(/[0-9]+/)
let initFilter = curry(function (fn,array) {
return array.filter(fn)
})
let findNumberInArray = initFilter(hasNumber)
console.log(findNumberInArray(['aaa', 'bb2', '33c', 'ddd', ]))
// 打印 [ 'bb2', '33c' ]
複製代碼
咱們上面設計的柯里化函數老是在最後接受一個數組,這使得它能接受的參數列表只能是從最左到最右。
可是有時候,咱們不能按照從左到右的這樣嚴格傳入參數,或者只是想部分地應用函數參數。這裏咱們就須要用到偏應用這個概念,它容許開發者部分地應用函數參數。
const partial = function (fn, ...partialArgs) {
return function (...fullArguments) {
let args = partialArgs
let arg = 0;
for(let i = 0; i < args.length && arg < fullArguments.length; i++) {
if(args[i] === undefined) {
args[i] = fullArguments[arg++]
}
}
return fn.apply(null,args)
}
}
複製代碼
偏應用的示例:
// 打印某個格式化的JSON
let prettyPrintJson = partial(JSON.stringify,undefined,null,2)
console.log(prettyPrintJson({name:'fangxu',gender:'male'}))
// 打印出
{
"name": "fangxu",
"gender": "male"
}
複製代碼
const compose = (...fns) => {
return (value) => reduce(fns.reverse(),(acc,fn) => fn(acc), value)
}
複製代碼
compose 組合的函數,是按照傳入的順序從右到左調用的。因此傳入的 fns 要先 reverse 一下,而後咱們用到了reduce ,reduce 的累加器初始值是 value ,而後會調用 (acc,fn) => fn(acc)
, 依次從 fns 數組中取出 fn ,將累加器的當前值傳入 fn ,即把上一個函數的返回值傳遞到下一個函數的參數中。
組合的實例:
let splitIntoSpace = (str) => str.split(' ')
let count = (array) => array.length
const countWords = composeN(count, splitIntoSpace)
console.log(countWords('make smaller or less in amount'))
// 打印 6
複製代碼
compose 函數的數據流是從右往左的,最右側的先執行。固然,咱們還可讓最左側的函數先執行,最右側的函數最後執行。這種從左至右處理數據流的過程稱爲管道(pipeline)或序列(sequence)。
// 跟compose的區別,只是沒有調用fns.reverse()
const pipe = (...fns) => (value) => reduce(fns,(acc,fn) => fn(acc),value)
複製代碼
定義:函子是一個普通對象(在其它語言中,多是一個類),它實現了map函數,在遍歷每一個對象值的時候生成一個新對象。
一、簡言之,函子是一個持有值的容器。並且函子是一個普通對象。咱們就能夠建立一個容器(也就是對象),讓它可以持有任何傳給它的值。
const Container = function (value) {
this.value = value
}
let testValue = new Container(1)
// => Container {value:1}
複製代碼
咱們給 Container 增長一個靜態方法,它能夠爲咱們在建立新的 Containers 時省略 new 關鍵字。
Container.of = function (value) {
return new Container(value)
}
// 如今咱們就能夠這樣來建立
Container.of(1)
// => Container {value:1}
複製代碼
二、函子須要實現 map 方法,具體的實現是,map 函數從 Container 中取出值,傳入的函數把取出的值做爲參數調用,並將結果放回 Container。
爲何須要 map 函數,咱們上面實現的 Container 僅僅是持有了傳給它的值。可是持有值的行爲幾乎沒有任何應用場景,而 map 函數發揮的做用就是,容許咱們使用當前 Container 持有的值調用任何函數。
Container.prototype.map = function (fn) {
return Container.of(fn(this.value))
}
// 而後咱們實現一個數字的 double 操做
let double = (x) => x + x;
Container.of(3).map(double)
// => Container {value: 6}
複製代碼
三、map返回了一傳入函數的執行結果爲值的 Container 實例,因此咱們能夠鏈式操做。
Container.of(3).map(double).map(double).map(double)
// => Container {value: 24}
複製代碼
經過以上的實現,咱們能夠發現,函子就是一個實現了map契約的對象。函子是一個尋求契約的概念,該契約很簡單,就是實現 map 。根據實現 map 函數的方式不一樣,會產生不一樣類型的函子,如 MayBe 、 Either
函子能夠用來作什麼?以前咱們用tap函數來函數式的解決代碼報錯的調試問題,如何更加函數式的處理代碼中的問題,那就須要用到下面咱們說的MayBe函子
讓咱們先寫一個upperCase函數來假設一種場景
let value = 'string';
function upperCase(value) {
// 爲了不報錯,咱們得寫這麼一個判斷
if(value != null || value != undefined)
return value.toUpperCase()
}
upperCase(value)
// => STRING
複製代碼
如上面所示,咱們代碼中常常須要判斷一些null
和undefined
的狀況。下面咱們來看一下MayBe函子的實現。
// MayBe 跟上面的 Container 很類似
export const MayBe = function (value) {
this.value = value
}
MayBe.of = function (value) {
return new MayBe(value)
}
// 多了一個isNothing
MayBe.prototype.isNoting = function () {
return this.value === null || this.value === undefined;
}
// 函子一定有 map,可是 map 的實現方式可能不一樣
MayBe.prototype.map = function(fn) {
return this.isNoting()?MayBe.of(null):MayBe.of(fn(this.value))
}
// MayBe應用
let value = 'string';
MayBe.of(value).map(upperCase)
// => MayBe { value: 'STRING' }
let nullValue = null
MayBe.of(nullValue).map(upperCase)
// 不會報錯 MayBe { value: null }
複製代碼
MayBe.of("tony")
.map(() => undefined)
.map((x)f => "Mr. " + x)
複製代碼
上面的代碼結果是 MyaBe {value: null}
,這只是一個簡單的例子,咱們能夠想一下,若是代碼比較複雜,咱們是不知道究竟是哪個分支在檢查 undefined 和 null 值時執行失敗了。這時候咱們就須要 Either 函子了,它能解決分支拓展問題。
const Nothing = function (value) {
this.value = value;
}
Nothing.of = function (value) {
return new Nothing(value)
}
Nothing.prototype.map = function (fn) {
return this;
}
const Some = function (value) {
this.value = value;
}
Some.of = function (value) {
return new Some(value)
}
Some.prototype.map = function (fn) {
return Some.of(fn(this.value));
}
const Either = {
Some,
Nothing
}
複製代碼
函子只是一個實現了 map 契約的接口。Pointed 函子也是一個函子的子集,它具備實現了 of 契約的接口。 咱們在 MayBe 和 Either 中也實現了 of 方法,用來在建立 Container 時不使用 new 關鍵字。因此 MayBe 和 Either 均可稱爲 Pointed 函子。
ES6 增長了 Array.of, 這使得數組成爲了一個 Pointed 函子。
MayBe 函子極可能會出現嵌套,若是出現嵌套後,咱們想要繼續操做真正的value是有困難的。必須深刻到 MayBe 內部進行操做。
let joinExample = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
// 這個時候咱們想讓5加上4,須要深刻 MayBe 函子內部
joinExample.map((insideMayBe) => {
return insideMayBe.map((value) => value + 4)
})
// => MayBe { value: MayBe { value: 9 } }
複製代碼
咱們這時就能夠實現一個 join 方法來解決這個問題。
// 若是經過 isNothing 的檢查,就返回自身的 value
MayBe.prototype.join = function () {
return this.isNoting()? MayBe.of(null) : this.value
}
複製代碼
let joinExample2 = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
// 這個時候咱們想讓5加上4就很簡單了。
joinExample2.join().map((value) => value + 4)
// => MayBe { value: 9 }
複製代碼
再延伸一下,咱們擴展一個 chain 方法。
MayBe.prototype.chain = function (fn) {
return this.map(fn).join()
}
複製代碼
調用 chain 後就能把嵌套的 MayBe 展開了。
let joinExample3 = MayBe.of(MayBe.of(5));
// => MayBe { value: MayBe { value: 5 } }
joinExample3.chain((insideMayBe) => {
return insideMayBe.map((value) => value + 4)
})
// => MayBe { value: 9 }
複製代碼
Monad 其實就是一個含有 chain 方法的函子。只有of 和 map 的 MayBe 是一個函子,含有 chain 的函子是一個 Monad。
函數式編程主張函數必須接受至少一個參數並返回一個值,可是JavaScript容許咱們建立一個不接受參數而且實際上什麼也不返回的函數。因此JavaScript不是一種純函數語言,更像是一種多範式的語言,不過它很是適合函數式編程範式。
function generateGetNumber() {
let numberKeeper = {}
return function (number) {
return numberKeeper.hasOwnProperty(number) ?
number :
numberKeeper[number] = number + number
}
}
const getNumber = generateGetNumber()
getNumber(1)
getNumber(2)
……
getNumber(9)
getNumber(10)
// 此時numberKeeper爲:
{
1: 2
2: 4
3: 6
4: 8
5: 10
6: 12
7: 14
8: 16
9: 18
10: 20
}
複製代碼
如今咱們規定,getNumber只接受1-10範圍的參數,那麼返回值確定是 numberKeeper 中的某一個 value 。據此咱們分析一下 getNumber ,該函數接受一個輸入併爲給定的範圍(此處範圍是10)映射輸出。輸入具備強制的、相應的輸出,而且也不存在映射兩個輸出的輸入。
下面我來再看一下數學函數的定義(維基百科)
在數學中,函數是一種輸入集合和可容許的輸出集合之間的關係,具備以下屬性:每一個輸入都精確地關聯一個輸出。函數的輸入稱爲參數,輸出稱爲值。對於一個給定的函數,全部被容許的輸入集合稱爲該函數的定義域,而被容許的輸出集合稱爲值域。
根據咱們對於 getNumber 的分析,對照數學函數的定義,會發現徹底一致。咱們上面的getNumber函數的定義域是1-10,值域是2,4,6,……18,20
文中全部的概念對應的實例能夠在 github.com/qiqihaobenb… 獲取,能夠打開對應的註釋來實際執行一下。
《JavaScript ES6 函數式編程入門經典》,強烈建議想入門函數式編程的同窗看一下,書有點老,能夠略過工具介紹之類的,關鍵看其內在的思想,最重要的是,這本書很薄,差很少跟一本漫畫書相似。