第一章:
函數式編程主要基於數學函數和它的思想。
1.1 函數與js方法:
函數是一段可以通過其名稱被調用的代碼,可以傳遞參數並返回值。
方法是一段必須通過其名稱及其關聯對象的名稱被調用的代碼。
//函數 var func = (a)=>{return a} func(5) //用其名稱調用 //方法 var obj = {simple:(a)=>{return a}} obj.simple(5) //用其名稱及其關聯對象調用
1.2 引用透明性
所有函數對於相同的輸入都將返回相同的值(函數只依賴參數的輸入,不依賴於其他全局數據,即函數內部沒有全局引用),這使並行代碼和緩存(用值直接替換函數的結果)成為可能。
1.3 命令式、聲明式與抽象
命令式主張告訴編譯器“如何”做
var array = [1,2,3] for(let i=0; o<array.length;i++){ console.log(array[i]) }
聲明式告訴編譯器“做什么”,如何做的部分(獲得數組長度,循環遍歷每一項)被抽象到高階函數中,forEach就是這樣一個內置函數,本書中我們都將創建這樣的內置函數。
var array = [1,2,3] array.forEach(elememt=>console.log(elememt))
1.4 函數式編程的好處&純函數
好處就是編寫純函數。純函數是對相同輸入返回相同輸出的函數,不依賴(包含)任何外部變量,所以也不會產生改變外部環境變量的副作用。
1.5 並行代碼
純函數允許我們並行執行代碼,因為純函數不會改變它的環境,所以不需要擔心同步問題。當然,js並沒有真正的多線程支持並行,但如果你的項目使用了webworker來模擬多線程並行執行任務,這種時候就需要用純函數來代替非純函數。
let global = 'something'; let function1 = (input) => { global = "somethingElse" } let function2 = ()=>{ if(global === 'something'){ //業務邏輯 } }
如果我們要兩個線程並行執行function1和function2,由於兩個函數都依賴全局變量global,並行執行就會引起不良的影響(兩個函數的執行順序不同會有不同的結果),現在把它們改為純函數。
let function1 = (input,global)=>{ global = "somethingElse" } let function2 = (global)=>{ if(global === "something"){ //業務邏輯 } }
我們移動了global變量,把它作為兩個函數的參數,使他們變成純函數。現在並行執行不會有任何問題,由於函數不依賴於外部環境變量,不必擔心線程的執行順序。
1.6 可緩存
根據純函數對於給定輸入總是返回相同的輸出,我們可以緩存函數的輸出,減少多次的輸入來反復調用函數。
var longRunningFnBookKeeper = {2:3,4:5...} longRunningFnBookKeeper.hasOwnProperty(ip)?longRunningFnBookKeeper[ip]:longRunningFunction(ip)
1.7 管道與組合
純函數應該被設計為:只做一件事。實現多個功能通過函數的組合來實現。
UNIX/LINUX中,在一個文件中找到一個特定的名稱並統計它的出現次數:
cat jsBook | grep -i "composing" | wc
組合不是命令行特有的,它是函數式編程的核心。
1.8 關於js
js是一門面對對象的語言,不是一種純函數語言,更像是一種多范式語言,但是非常適合函數式編程。
第二章:js函數基礎
今天很多瀏覽器還不支持ES6,我們可以通過轉換編譯器babel,將ES6轉換為ES5代碼。

可以看到,箭頭函數的this經過編譯后為undefined,轉換后的代碼運行在嚴格模式下,嚴格模式是js的受限變體。
"use strict" a = 1 // -> Uncaught ReferenceError: a is not defined; 此處直接報錯
在函數內部如果用var聲明變量和不用時有很大差別,用var聲明的是局部變量,在函數外部訪問這個變量是訪問不到的,沒var聲明的是全局變量。在函數外部是可以訪問到的。
如果你不使用var命令指定,在全局狀態下定一個變量。在嚴格模式下這段代碼會報錯,因為全局變量在js中非常有害。
第三章:高階函數
高階函數(HOC):
- 接收函數作為參數
- 返回函數作為輸出
- 接收函數作為參數且返回函數作為輸出
滿足以上三個之一的函數就是高階函數。
3.1 理解數據
3.1.1 js中函數為一等公民:
因為函數也是js中的一種數據類型,可以被賦值給變量,作為參數傳遞,也可被其他函數返回。
3.1.2 把一個函數存入變量
let fn = () => {} //fn就是一個指向函數數據類型的變量,即函數的引用 fn() //調用函數,即執行fn指向的函數
3.1.3 函數作為參數傳入
var tellType = arg =>{ if(typeof arg==='function'){ arg() //如果傳入的是函數就執行 }else{ console.log(arg) //否則就輸出數據 } } var fn = () => {console.log('i am a function')} tellType(fn) //函數作為參數傳入
3.1.4 返回函數
String是js的內置函數,注意:只返回了函數的引用,並沒有執行函數
let crazy = () =>{ return String } crazy() // String() { [native code] } crazy()('HOC') // "HOC"
3.2 抽象和高階函數
高階函數就是定義抽象
3.2.1通過高階函數實現抽象
forEach實現遍歷數組
const forEach = (array,fn)=>{ for(let i=0;i<array.length;i++){ fn(array[i]) } }
forEachObject實現遍歷對象
const forEachObject = (obj,fn)=>{ for(var property in obj){ if(obj.hasOwnProperty(properity){ fn(property,obj[property]) }) } }
注意:forEach和forEachObject都是高階函數,他們使開發者專注於任務,而抽象出遍歷的部分。
unless函數:如果predicate為false,則調用fn
const unless = (predicate,fn)=>{ if(!predicate) fn() }
查找一個列表中的偶數
forEach([1,2,3,4,6,7],(number)=>{ unless((number%2),()=>{ console.log(number,"is even") }) })
如果我們操作的是一個Number而不是array
const times = (time,fn)=>{ for(var i=0;i<time;i++){ fn(i) } } times(100,function(n){ unless(n%2,function(){ console.log(n, "is even") }) })
3.3 真實的高階函數
a.every(function(element, index, array))
every是所有函數的每個回調函數都返回true的時候才會返回true,當遇到false的時候終止執行,返回false。
a.some(function(element, index, array))
some函數是“存在”有一個回調函數返回true的時候終止執行並返回true,否則返回false
在空數組上調用every返回true,some返回false。
3.3.1 every函數
every函數接受兩個參數:一個數組和一個函數。它使用傳入的函數檢查數組的所有元素是否為true, 都為true才返回true
const every = (arr,fn)=>{ let result = true; for(let i =0;i<arr.length;i++){ result = result&&fn(arr[i]) } return true; } every([NaN,NaN,NaN],isNaN)
for..of循環:ES6中用於遍歷數組元素的方法,重寫every方法
const every = (arr,fn)=>{ let result = true; for(const element of arr){ result = result&&fn(element) } return true; }
3.3.2 some函數
some函數接受兩個參數:一個數組和一個函數。它使用傳入的函數檢查數組的所有元素是否為true, 只要有一個為true就返回true
const some = (arr,fn)=>{ let result = false; for(const element of arr){ result = result||fn(element) } return true; } some([5,NaN,NaN],isNaN)
3.3.3 sort函數
sort函數是一個高階函數,它接受一個函數作為參數,該函數幫助sort函數決定排序邏輯, 是一個改變原數組的方法。
arr.sort([compareFunc])
compareFunc是可選的,如果compareFunc未提供,元素將被轉換為字符串並按Unicode編碼點順序排列。
compareFunc應該實現下面的邏輯
function compareFunc(a,b){ if(根據某種排序標准a<b){ return -1 } if(根據某種排序標准a>b){ return 1 } return 0; }
具體例子
var friends = [{name: 'John', age: 30}, {name: 'Ana', age: 20}, {name: 'Chris', age: 25}];
function compareFunc(a,b){ return (a.age<b.age)?-1:(a.age>b.age)?1:0 }
寫成以下也ok,按照age升序排列
function compareFunc(a,b){ return a.age>b.age }
friends.sort(compareFunc)

如果要比較不同的屬性,我們需要重復編寫比較代碼。下面新建一個sortBy函數,允許用戶基於傳入的屬性對對象數組排序。
const sortBy = (property)=>{ return (a,b) => { return (a[property]<b[property])?-1:(a[property]>b[property])?1:0 } }
var friends = [{name: 'John', age: 30}, {name: 'Ana', age: 20}, {name: 'Chris', age: 25}];
friends.sort(sortBy('age'))
注意:sortBy函數接受一個屬性冰返回另一個函數,這個返回的函數就作為compareFunc傳遞給sort函數,持有property參數值的返回函數之所以能夠運行是因為js支持閉包。
第四章:高階函數與閉包
4.1理解閉包
4.1.1什么是閉包
簡言之,閉包是一個內部函數,它是在一個函數內部的函數。
function outer(){ function inner(){ } }
函數inner稱為閉包函數,閉包如此強大的原因在於它對作用域鏈的訪問。
閉包有3個可以訪問的作用域:
1.閉包函數內聲明的變量
2.對全局變量的訪問
3.對外部函數變量的訪問!!!!
let global = 'global';//2 function outer(){ let outer = 'outer'; function inner(){ let a=5;//1 console.log(outer) //3.閉包能夠訪問外部函數變量 } return inner } outer()()//"outer"
4.1.2 閉包可以記住它的上下文
var fn = (arg)=>{ let outer = 'outer'; let innerFn = () =>{ console.log(outer) console.log(arg) } return innerFn } var closeureFn = fn(5) closeureFn()//outer 5
當執行var closeureFn = fn(5)
時,函數innerFn被返回,js執行引擎視innerFn為一個閉包,並相應的設置了它的作用域。3個作用域層級在innerFn返回時都被設置了。
如此,closeureFn()通過作用域鏈被調用時就記住了arg、outer的值。
我們回到sortBy
const sortBy = (property)=>{ return (a,b) => { return (a[property]<b[property])?-1:(a[property]>b[property])?1:0 } }
當我們以如下形式調用時
sortBy('age')
發生下面的事情:
sortBy函數返回了一個接受兩個參數的新函數,這個新函數就是一個閉包
(a,b)=>{/*實現*/}
根據閉包能訪問作用域層級的特點,它能在它的上下文中持有property的值,所以它將在合適並且需要的時候使用返回值。
4.2真實的高階函數
4.2.1 once:允許只運行一次給定的函數
這在開發過程中很常見,例如只想設置一次第三方庫,初始化一次支付設置。
const once = (fn)=>{ let done = false; return function(){ return done?undefined:((done=true),fn.apply(this,arguments)) } }
var dopayment = once(()=>{console.log("Payment is done")}) dopayment() //Payment is done dopayment() //undefined
js中,(exp1,exp2)的含義是執行兩個參數並返回第二個表達式的結果。
注意:once函數接受一個參數fn並通過調用fn的apply方法返回結果。我們聲明了done變量,返回的函數會形成一個覆蓋它的閉包作用域,檢查done是否為true,如果是則返回undefined,
否則將done設為true,如此就阻止了下一次的執行。
4.2.2 memoized
用於為每一個輸入存儲結果,以便於重用函數中的計算結果。
const memoized = (fn) => { const lookupTable = {}; return (arg) => lookupTable[arg] || (lookupTable[arg]=fn(arg)); }
有一個名為lookupTable的局部變量,它在返回函數的閉包上下文中。返回函數將接受一個參數並檢查它是否在lookupTable中。
如果在,就返回對應的值,否則使用新的輸入作為key,fn(arg)的結果為value,更新lookupTable對象。
求函數的階乘(遞歸法)
var factorial = (n) => { if(n===0){ return 1; } return n*factorial(n-1) }
現在可以改為把factorial函數包裹進一個memoized函數來保留它的輸出(存儲結果法)
let factorial = memoized((n)=>{ if(n===0){ return 1; } return n*factorial(n-1) })
它以同樣的方式運行,但是比之前快的多。
第五章:數組的函數式編程
我們使用數組來存儲、操作和查找數據,以及轉換(投影)數據格式。本章中使用函數式編程來改進這些操作。
5.1 數組的函數式方法
本節創建的所有函數稱為投影函數,把函數應用於一個值並創建一個新值的過程稱為投影。
5.1.1 map
首先來看遍歷數組的forEach方法
const forEach = (array,fn) => { for(const value of array) fn(value) }
map函數的實現代碼如下
const map = (array,fn) => { let results= []; for(const value of array) results.push(fn(value)) return results; }
map和forEach非常類似,區別是用一個新的數組捕獲了結果,並返回了結果。
let apressBooks = [ { "id": 111, "title": "c# 6.0", "author": "ANDREW JKDKS", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW JKDKS", "rating": [3], "reviews": [{good:4, excellent: 12}] }, { "id": 333, "title": "Angularjs", "author": "ANDREW JKDKS", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW JKDKS", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ];
假設只需要獲取包含title和author的字段
map(apressBooks,(book)=>{ return {title:book.title,author:book.author} })
5.1.2 filter
有時我們還想過濾數組的內容(例如獲取rating>4.5的圖書列表),再轉換為一個新數組,因此我們需要一個類似map的函數,它只需要在把結果放入數組前檢查一個條件。
const filter = (array,fn) => { let results= []; for(const value of array) fn(value) ? results.push(value) : undefined return results; }
調用高階函數filter
filter(apressBooks, (book)=>book.rating[0]>4.5)
返回結果

5.2 連接操作
map和filter都是投影函數,因此它們總是對數組應用轉換操作后再返回數據,於是我們能夠連接filter和map(注意順序)來完成任務而不需要額外變量。
例如:從apressBooks中獲取含有title和author對象且評級高於4.5的對象。
map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{ return {title:book.title,author:book.author} })
我們將后面的章節中國通過函數組合來完成同樣的事情。
concatAll
對apressBooks對象稍作修改,得到如下數據結構
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [{good:4, excellent: 12}] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ] } ];
現在回顧上一節的問題:獲取含有title和author字段且評級高於4.5的圖書。
map(apressBooks,(book)=>{ return book.bookDetails })
得到如下輸出

如上圖所示,map函數返回的數據包含了數組中的數組,如果把上面的數據傳給filter將會遇到問題,因為filter不能在嵌套數組上運行。
我們定義一個concatAll函數把所有嵌套數組連接到一個數組中,也可稱concatAll為flatten方法(嵌套數組平鋪)。concatAll的主要目的是將嵌套數組轉換為非嵌套的單一數組。
const concatAll = (array) => { let results = []; for(const value of array){ results.push.apply(results,value) //重點!! } return results; }
使用js的apply方法,將push的上下文設置為results
concatAll(map(apressBooks,(book)=>{ return book.bookDetails }))
返回了我們期望的結果(數組平鋪)

轉換為非嵌套的單一數組后就可以繼續使用filter啦
filter(
concatAll(map(apressBooks,(book)=>{
return book.bookDetails })),(book) => (book.rating[0] > 4.5) )
返回結果

flatten嵌套數組扁平化
let arr = [[1,2,[3,4]],[4,5],77]
遍歷每一項,如果仍是數組的話就遞歸調用flatten,並將結果與result concat一下。如果不是數組就直接push該項到result。
function flatten(array){ var result = []; var toStr = Object.prototype.toString; for(var i=0;i<array.length;i++){ var element = array[i]; if(toStr.call(element) === "[object Array]"){ //Array.isArray(element) === true result = result.concat(flatten(element)); //[...result,...flatten(element)] } else{ result.push(element); } } return result; } let results = flatten(arr)
5.3 reduce函數
reduce為保持Javascript閉包的能力所設計。
先來看一個數組求和問題:
let useless = [2,5,6,1,10] let result = 0; forEach(useless,value=>{ result+=value; }) console.log(result) //24
對於上面的問題,我們將數組歸約為一個單一的值,從一個累加器開始(result),在遍歷數組時使用它存儲求和結果。
歸約數組:設置累加器並遍歷數組(記住累加器的上一個值)以生成一個單一元素的過程稱為歸約數組。
我們將這種歸約操作抽象成reduce函數。
reduce函數的第一個實現
const reduce = (array,fn)=>{ let accumlator = 0; for(const value of array){ accumlator = fn(accumlator,value); } return [accumlator] } reduce(useless,(acc,val)=>acc+val) //[24]
但如果我們要求給定數組的乘積,reduce函數會執行失敗,主要是因為我們使用了累加器的值0。
我們修改reduce函數,讓它接受一個為累加器設置初始值的參數。
如果沒有傳遞initialValue時,則以數組的第一個元素作為累加器的值。
const reduce = (array,fn,initialValue)=>{ let accumlator; if(initialValue != undefined) accumlator = initialValue; else accumlator = array[0]; //當initialValue未定義時,我們需要從第二個元素開始循環數組 if(initialValue === undefined){ for(let i=1; i<array.length;i++){ accumlator = fn(accumlator,array[i]) } }else{//如果initialValue由調用者傳入,我們就需要遍歷整個數組。 for(const value of array){ accumlator = fn(accumlator,value); } } return [accumlator] }
嘗試通過reduce函數解決乘積問題
let useless = [2,5,6,1,10] reduce(useless,(acc,val)=>acc*val,1) //[600]
reduce使用舉例
從apressBooks中統計評價為good和excellent的數量。->使用reduce
由於apressBooks包含數組中的數組,先需要使用concatAll把它轉化為一個扁平的數組。
concatAll(map(apressBooks,(book)=>{ return book.bookDetails }))
我們使用reduce解決該問題。
let bookDetails = concatAll(map(apressBooks,(book)=>{ return book.bookDetails })) reduce(bookDetails,(acc,bookDetail)=>{ let goodReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0 let excellentReviews = bookDetail.reviews[0] != undefined ? bookDetail.reviews[0].good : 0 return {good:acc.good + goodReviews, excellent:acc.excellent + excellentReviews} },{good:0,excellent:0})
在reduce函數體中,我們獲取good和excellent的評價詳情,將其存儲在相應的變量中,名為goodReviews和excellentReviews。
5.4 zip數組
再回顧一下之前數據的結構,我們在apressBooks的bookDetails中獲取reviews,並能輕松的操作它。
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4], "reviews": [{good:4, excellent: 12}] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [{good:4, excellent: 12}] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [{good:4, excellent: 12}] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7], "reviews": [{good:4, excellent: 12}] } ] } ];
但是有時候數據可能被分離到不同部分了。
let apressBooks = [ { name: "beginners", bookDetails: [ { "id": 111, "title": "c# 6.0", "author": "ANDREW 1", "rating": [4] }, { "id": 222, "title": "Machine Learning", "author": "ANDREW 2", "rating": [3], "reviews": [] } ] }, { name: "pro", bookDetails: [ { "id": 333, "title": "Angularjs", "author": "ANDREW 3", "rating": [5], "reviews": [] }, { "id": 444, "title": "Pro ASP.NET", "author": "ANDREW 4", "rating": [4.7] } ] } ];
reviews被填充到一個單獨的數組中。
let reviewDetails = [
{
"id":111, "reviews":[{good:4,excellent:12}] }, { "id":222, "reviews":[] }, { "id":111, "reviews":[] }, { "id":111, "reviews":[{good:4,excellent:12}] }, ]
zip函數
const zip = (leftArr,rightArr,fn) => { let index,results=[]; for(index=0;index<Math.min(leftArr.length,rightArr.length);index++){ results.push(fn(leftArr[index],rightArr[index])); } return results; }
zip:我們只需要遍歷兩個給定的數組,由於要處理兩個數組詳情,就需要用 Math.min 獲取它們的最小長度Math.min(leftArr.length, rightArr.length)
,一旦獲取了最小長度,我們就能夠用當前的leftArr值和rightArr值調用傳入的高階函數fn。
假設我們要把兩個數組的內容相加,可以采用如下方式使用zip
zip([1,2,3],[4,5,6],(x,y)=>x+y)
繼續解決上一節的問題:統計Apress出版物評價為good和excellent的總數。
我們接受bookDetails和reviewDetails數組,檢查兩個數組元素的id是否匹配,如果是,就從book中克隆出一個新的對象clone
//獲取bookDetails let bookDetails = concatAll(map(apressBooks,(book)=>{ return book.bookDetails })) //zip results let mergedBookDetails = zip(bookDetails, reviewDetails, (book, review)=>{ if(book.id === review.id){ let clone = Object.assign({},book) clone.ratings = review //為clone添加一個ratings屬性,以review對象作為其值 return clone } })
注意:Object.assign(target, ...sources)
Object.assign() 方法用於將所有可枚舉屬性的值從一個或多個源對象復制到目標對象。它將返回目標對象。
let clone = Object.assign({},book)
clone得到了一份book 對象的副本,clone指向了一個獨立的引用,為clone添加屬性或操作不會改變真實的book引用。

第六章:柯里化與偏應用
6 一些術語
6.1 一元函數
只接受一個參數的函數稱為一元(unary)函數。
const identity = (x) => x
6.2 二元函數
接受兩個參數的函數稱為二元(binary)函數。
const add = (x,y) => x+y;
6.1 變參函數
指函數接受的參數數量是可變的。ES5中我們通過arguments來捕獲可變數量的參數。
function variadic(a){ console.log(a) console.log(arguments) }
調用
variadic(1,2,3) 1 [1,2,3]
ES6中我們使用擴展運算符,獲得可變參數
const variadic = (a,...variadic){ console.log(a) console.log(variadic) }
調用
variadic(1,2,3) 1 [2, 3]
6.2 柯里化
柯里化:把一個多參數函數轉換為一個嵌套的一元函數的過程。
看個例子,假設有一個名為add的函數
const add = (x,y)=>x+y;
我們會如此調用該函數add(1,1),得到結果2。下面是add函數的柯里化版本:
const addCurried = x => y => x+y;
如果我們用一個單一的參數調用addCurried,
addCurried(3)
它返回一個函數,在其中x值通過閉包被捕獲,fn = y => 4+y
,因此可以用如下方式調用addCurried
addCurried(3)(4) //7
類似的乘法函數
const curri = x=>y=>z=>x*y*z; curri(2)(3)(4) //24
下面展示了如何把該處理過程轉換為一個名為curry的方法, curry方法將接收到的函數參數curry化
const curry = (binaryFn) => { return function(firstArg){ return function(secondArg){ return binaryFn(firstArg,secondArg) } } }
調用curry函數,curry化add。
const add = (x,y)=>x+y; let autoCurried = curry(add) autoCurried(2)(3) //5
6.2.1 柯里化用例
假設我們要編寫一個創建列表的函數,創建列表tableOf2、tableOf3、tableOf4等。
const tableOf2 = (y) => 2*y; const tableOf3 = (y) => 3*y; const tableOf4 = (y) => 4*y;
現在可以把表格的概念概括為一個單獨的函數
const genericTable = (x,y) => x*y
我們將genericTable柯里化,用2填充tableOf2的第一個參數,用3填充tableOf3的第一個參數,用4填充tableOf4的第一個參數。
const tableOf2 = curry(genericTable)(2); const tableOf3 = curry(genericTable)(3); const tableOf4 = curry(genericTable)(4);
6.2.2 完整curry函數
添加規則,檢查如果傳入參數不是function,就會報錯。
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } }
如果有人為柯里化函數提供了所有的參數,就需要通過傳遞這些參數執行真正的函數,重點在於返回函數curriedFn是一個變參函數。
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } return function curriedFn(){ //返回函數是一個變參函數 return fn(...arguments) } //采用如下寫法也ok // return function curriedFn(...args){ // return fn(...args) // } }
如果我們有一個名為multiply的函數:
const multiply = (x,y,z) => x*y*z;
可以通過如下方式調用,等價於multiply(1,2,3)
curry(multiply)(1,2,3) //6
下面回到把多參數函數轉換為嵌套的一元函數(柯里化的定義)
let curry = (fn) => { if(typeof fn!=='function'){ throw Error('No function provided') } return function curriedFn(...args){ //args是一個數組 if(args.length < fn.length){ //檢查...args傳入的參數長度是否小於函數參數列表的長度 return function(){ args = [...args,...arguments] return curriedFn(...args) }; } return fn(...args) //不小於,就和之前一樣調用整個函數 } }
args.length < fn.length
檢查...args傳入的參數長度是否小於函數參數列表的長度,如果是,就進入if代碼塊,如果不是就如之前一樣調用整個函數。
args = [...args,...arguments]
用來連接一次傳入的參數,把他們合並進args,並遞歸調用curriedFn。由於我們將所有傳入的參數 組合並遞歸地調用,再下一次調用中將會遇到某一個時刻if(args.length < fn.length)
條件失敗,說明這時args存放的參數列表的長度和函數參數的長度相等,程序就會被調用 fn(...args)。
調用
const multiply = (x,y,z) => x*y*z; curry(multiply)(1)(2)(3) //6
6.2.3 日志函數 —— 柯里化的應用
開發者在寫代碼時候會在應用的不同階段編寫很多日志。我們編寫如下日志函數。
6.3 柯里化實戰
6.3.1 在數組內容中查找數字
在數組中查找數字,返回包含數字的數組內容。
無需柯里化時,我們可以如下實現。
["js","number1"].filter(function(e){ return /[0-9]+/.test(e) //["number1"] })
采用柯里化的filter函數
let filter = curry((fn,ary)=>{ return ary.filter(fn) }) filter(function(str){ return /[0-9]+/.test(str); })(["js","number1"]) //["number1"]
6.3.2 求數組的平方
前幾章中,我們使用map函數傳入一個平凡函數來解決問題,此處可以通過curry函數以另一種方式解決該問題。
let map = curry(function(f,ary){ return ary.map(f) }) map(x=>x*x)([1,2,3]) //[1, 4, 9]
6.4 數據流
我們設計的柯里化函數總在最后接受數組,這是有意而為之。如果我們希望最后接受的參數是位於參數列表的中間某位置呢?curry就幫不了我們了。
6.4.1偏應用
偏應用:部分地應用函數參數。有時填充函數的前兩個參數和最后一個參數會使中間的參數處於一種未知狀態,這正是偏應用發揮作用的地方,將未知狀態的參數填充為undefined,之后填入其他參數調用函數。
setTimeout(()=>console.log("Do X task"),10) setTimeout(()=>console.log("Do Y task"),10)
我們為每一個setTimeout函數都傳入了10,我們希望把10作為常量,在代碼中把它隱藏。curry函數並不能幫我們解決這個問題,原因是curry函數應用參數列表的順序是從最左到最右。
一個方案是把setTimeout封裝一下,如此函數參數就會變成最右邊的一個。
const setTimeoutWrapper = (time,fn)=>{ setTimeout(fn,time); }
然后就能通過curry函數來實現一個10ms的延遲了
const delayTenMs = curry(setTimeoutWrapper)(10) delayTenMs(()=>console.log("Do X task")) delayTenMs(()=>console.log("Do Y task"))
程序將以我們需要的方式運行,但問題是創建了setTimeoutWrapper這個封裝器,這是一種開銷。
6.4.2 實現偏函數(適用於任何含有多個參數的函數)
const partial = function(fn, ...partialArgs){ let args = partialArgs; return function(...fullArguments){ 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) } }
使用該偏函數
let delayTenMs = partial(setTimeout,undefined,10); delayTenMs(()=>console.log("Do Y task"))
說明:
我們調用
partial(setTimeout,undefined,10);
這將產生
let args = partialArgs = [undefined,10]
返回函數將記住args的值(閉包)
返回函數非常簡單,它接受一個名為fullArguments的參數。所以傳入()=>console.log("Do Y task")
作為參數,
在for循環中我們執行遍歷並為函數創建必需的參數數組
if(args[i]===undefined){ args[i] = fullArguments[arg++]; }
從i=0開始,
返回函數將記住args的值,返回函數非常簡單,它接受一個名為fullArguments的參數。所以傳入
fullArguments = [()=>console.log("Do Y task")]
在if循環內
args[0]===undefined=>true
args[0]=()=>console.log("Do Y task")
如此args就變成
[()=>console.log("Do Y task"),10]
可以看出,args指向我們期望的setTimeout函數調用所需的數組,一旦在args中有了必要的參數,就可以通過fn.apply(null,args)調用函數了。
partial應用
注意,我們可以將partial應用於任何含有多個參數的函數,看下面的例子。js中使用JSON.stringify() 方法將一個JavaScript值(對象或者數組)轉換為一個 JSON字符串。
JSON.stringify(value[, replacer[, space]])
value:
必需, 要轉換的 JavaScript 值(通常為對象或數組)。
replacer:
可選。用於轉換結果的函數或數組。
如果 replacer 為函數,則 JSON.stringify 將調用該函數,並傳入每個成員的鍵和值。使用返回值而不是原始值。如果此函數返回 undefined,則排除成員。根對象的鍵是一個空字符串:""。
如果 replacer 是一個數組,則僅轉換該數組中具有鍵值的成員。成員的轉換順序與鍵在數組中的順序一樣。
space:
可選,文本添加縮進、空格和換行符,如果 space 是一個數字,則返回值文本在每個級別縮進指定數目的空格,如果 space 大於 10,則文本縮進 10 個空格。space 也可以使用非數字,如:\t。
我們調用下面的函數做JSON的美化輸出。
let obj = {obj:"bar",bar:"foo"} JSON.stringify(obj,null,2); 輸出: "{ "obj": "bar", "bar": "foo" }"
可以看到stringify調用的最后兩個參數總是相同的“null,2”,我們可以用partial移除樣板代碼
let prettyPrintJson = partial(JSON.stringify, undefined, null, 2) prettyPrintJson({obj:"bar",bar:"foo"}) 輸出: "{ "obj": "bar", "bar": "foo" }"
該偏函數的小bug:
如果我們使用一個不同的參數再次調用prettyPrintJson,它將總是給出第一次調用的結果。
prettyPrintJson({obj:"bar",bar:"foo222"}) 輸出:總是給出第一次調用的結果 "{ "obj": "bar", "bar": "foo" }"
因為我們通過參數替換undefined值的方式修改partialArgs,而數組傳遞的是引用。
第七章:組合與管道(compose/pipe)
7.1 組合的概念
函數式組合:將多個函數組合在一起以便能構建出一個新函數。
Unix的理念
1.每個程序只做好一件事情。
2.每個程序的輸出應該是另一個尚不可知的程序的輸入。
Unix管道符號|
使用Unix管道符號|
,就可以將左側的函數輸出作為右側函數的輸入。
如果想計算單詞word在給定文本文件中的出現次數,該如何實現呢?
cat test.txt | grep 'world' | wc
cat用於在控制台現實文本文件的內容,它接受一個參數(文件位置)
grep在給定的文本中搜索內容
wc計算單詞在給定文本中的數量
7.2 compose函數
本節創建第一個compose函數,它需要接收一個函數的輸出,並將其作為輸入傳遞給另外一個函數。
const compose = (a, b)=>(c)=>a(b(c))
compose 接收函數a 、b作為輸入,並返回一個接收參數c的函數。當用c調用返回函數時,它將用輸入c調用函數b,b的輸出作為a的輸入,這就是compose函數的定義。
注意:函數的調用方向是從右至左的。
7.3 應用compose函數
例子1:對一個給定的數字四舍五入求和。
let data = parseFloat("3.56") let number = Math.round(data) //4
下面通過compose函數解決該問題:
const compose = (a, b)=>(c)=>a(b(c)) let number = compose(Math.round, parseFloat) number("3.56") //4
以上就是函數式組合,我們將兩個函數(Math.round、parseFloat)組合在一起以便能構造出一個新函數,注意:Math.round和parseFloat知道調用number函數時才會執行。
例子2:計算一個字符串中單詞的數量
已有以下兩個函數:
let splitIntoSpaces = (str) => str.split(" ") let count = (array) => array.length;
如果想用這兩個函數構建一個新函數,計算一個字符串中單詞的數量。
const countWords = compose(count, splitIntoSpaces)
調用
countWords("hello what's your name") // 4
7.3.1 引入curry和partial
以上的例子中,僅當函數接收一個參數時,我們才能將兩個函數組合。但還存在多參數函數的情況,我們可以通過curry和partial函數來實現。
5.2中,我們通過以下寫法從apressBooks中獲取含有title和author對象且評級高於4.5的對象。
map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{ return {title:book.title,author:book.author} })
本節使用compose函數將map和filter組合起來。
compose只能組合接受一個參數的函數,但是map和filter都接受兩個參數map(array,fn)
filter(array,fn)
(數組,操作數組的函數),不能直接將他們組合。我們使用partial函數部分地應用map和filter的第二個參數。
我們定義了過濾圖書的小函數filterGoodBooks和投影函數projectTitleAndAuthor
let filterGoodBooks = (book)=>book.rating[0]>4.5; let projectTitleAndAuthor = (book)=>{return {title:book.title, author:book.author}}
現在使用compose和partial實現
let queryGoodBooks = partial(filter, undefined, filterGoodBooks); let mapTitleAndAuthor = partial(map, undefined, projectTitleAndAuthor); let titleAndAuthorForGoodBooks = compose(mapTitleAndAuthor, queryGoodBooks)
使用
titleAndAuthorForGoodBooks(apressBooks) 輸出: 0: {title: "Angularjs", author: "ANDREW JKDKS"} 1: {title: "Pro ASP.NET", author: "ANDREW JKDKS"}
本例子使用partial和compose解決問題,也可以用curry來做同樣的事情。
提示:顛倒map和filter的參數順序。
const mapWrap = (fn,array)=>{ return map(array,fn) }
7.3.2 組合多個函數
當前的compose只能組合兩個給定的函數,我們重寫compose函數,使它能組合三個、四個、更多函數。
const compose = (...fns) => (value) => reduce(fns.reverse(), (acc, fn) => fn(acc), value)
reduce用於把數組歸約為一個單一的值,例如求給定數組的元素乘積,累乘器初始值為1
reduce([1,2,3,4],(acc,val)=>acc*val,1) //24
此處通過fns.reverse()反轉函數數組,(acc, fn) => fn(acc)以傳入的acc為參數依次調用每一個函數。累加器的初始值是value變量,它作為函數的第一個輸入。
上一節中,我們組合了一個函數用於計算給定字符串的單詞數。
let splitIntoSpaces = (str) => str.split(" ") let count = (array) => array.length; const countWords = compose(count, splitIntoSpaces) countWords("hello what's your name") // 4
假設我們想知道給定字符串的單詞數是基數還是偶數,而我們已經有如下函數
let oddOrEven = (ip) => ip%2 == 0 ? "even" : "odd";
通過compose,將這三個函數組合起來
const oddOrEvenWords = compose(oddOrEven, count, splitIntoSpaces); oddOrEvenWords("hello what's your name") // ["even"]
7.4 管道/序列
compose的數據流是從右至左的,最右側的函數會首先執行,將數據傳遞給下一個函數,以此類推...最左側的函數最后執行。
而當我們進行“|”操作時,Unix命令的數據流總是從左至右的,本節中,我們將實現pipe,它和compose函數所做的事情相同,只不過交換了數據流方向。
管道/序列(pipeline/sequence):從左至右處理數據流的過程稱為管道/序列。
7.4.1 實現pipe
pipe是compose的復制品,唯一修改的是數據流方向。
const pipe = (...fns) => (value) => reduce(fns, (acc, fn) => fn(acc), value)
此處沒有像compose一樣調用fns.reverse(),這意味着我們將按照原有順序執行函數。
調用pipe函數。注意,我們改變了調用順序,先splitIntoSpaces, 中count, 最后oddOrEven。
const oddOrEvenWords = pipe(splitIntoSpaces, count, oddOrEven); oddOrEvenWords("hello what's your name") // ["even"]
7.5 組合的優勢:結合律
函數式組合滿足結合律:
compose(compose(f,g),h) == compose(f,compose(g,h))
看一下上一節的例子
//compose(compose(f,g),h) const oddOrEvenWord1 = compose(compose(oddOrEven, count), splitIntoSpaces); oddOrEvenWord1("hello what's your name") //["even"] //compose(f,compose(g,h)) const oddOrEvenWord2 = compose(oddOrEven, compose(count, splitIntoSpaces)); oddOrEvenWord2("hello what's your name") //["even"]
真正的好處:把函數組合到各自所需的compose函數中,
let countWords = compose(count, splitIntoSpaces) let oddOrEvenWords = compose(oddOrEven, countWords)
or
let countOddOrEven= compose(oddOrEven, count) let oddOrEvenWords = compose(countOddOrEven, splitIntoSpaces)
第八章:函子
函子:用一種純函數式的方式進行錯誤處理。
8.1.1 函子是容器
函子是一個實現了map(遍歷每個對象值的時候生成一個新對象)的普通對象(在其他語言中可能是一個類)。簡而言之,函子是一個持有值的容器,能夠持有任何傳給它值,並允許使用當前容器持有的值調用任何函數。
創建Container構造函數
const Container = function(val){ this.value = val; }
不使用箭頭函數的原因是箭頭函數不具備內部方法Construct和prototype屬性,所以不能用new來創建一個新對象。
應用Container
let testValue = new Container(3) //Container {value: 3} let testObj = new Container({a:1}) //Container {value: {a: 1}} let testArray = new Container([1,2]) //Container {value: [1,2]}
我們為Container創建一個of靜態工具方法,用以代替new關鍵詞使用
Container.of = function(value){ return new Container(value) }
用of方法重寫上面的代碼
testValue = Container.of(3)
testObj = Container.of(3)
testArray = Container.of([1,2])
注意:Container也可以包含嵌套的 Container
Container.of(Container.of(33))
輸出:
Container { value: Container { value: 33 } }
8.1.2 函子實現了map方法
map方法允許我們使用當前Container持有的值調用任何函數。
即map函數從Container中取出值,將傳入的函數作用於該值,再將結果放回Container。
Container.prototype.map = function(fn){ return Container.of(fn(this.value)) }
第十章:使用Generator
Generator是ES6中關於函數的新規范。它不是一種函數式編程技術,但它是函數的一部分。
10.1 異步代碼及其問題(回調地獄)
同步VS異步
同步:函數執行時會阻塞調用者,並在執行完后返回結果。
異步:在執行時不會阻塞調用者,一旦執行完畢就會返回結果。
處理Ajax請求時就是在處理異步調用。
同步函數
let sync = () =>{ //一些操作 //返回數據 } let sync2 = () =>{ //一些操作 //返回數據 } let sync3 = () =>{ //一些操作 //返回數據 }
同步函數調用
result = sync()
result2 = sync2()
result3 = sync3()
異步函數
let async = (fn)=>{ //一些異步操作 //用異步操作調用回調 fn(/*結果數據*/) } let async2 = (fn)=>{ //一些異步操作 //用異步操作調用回調 fn(/*結果數據*/) } let async3 = (fn)=>{ //一些異步操作 //用異步操作調用回調 fn(/*結果數據*/) }
異步函數調用
async(function(x){ async2(function(y){ async3(function(z){ ... }) }) })
10.2 Generator基礎
Generator是ES6規范的一部分,被捆綁在語言層面。
10.2.1創建Generator
function* gen(){ return 'first generator'; } gen()
返回一個Generator原始類型的實例

調用實例的next函數,從該Generator實例中獲取值
gen().next() 輸出: {value: "first generator", done: true} gen().next().value 輸出: "first generator"
10.2.2 Generator的注意事項
一:不能無限制地調用next從Generator中取值
let genResult = gen() //第一次調用 genResult.next().value 輸出:"first generator" //第二次調用 genResult.next().value 輸出:undefined
原因是Generator如同序列,一旦序列中的值被消費,你就不能再次消費它。
本例中,genResult是一個帶有"first generator"值的序列,第一次調用next后,我們就已經從序列中消費了該值。
由於序列已為空,第二次調用它就會返回undefined。
為了能夠再次消費該序列,方法是創建另一個Generator實例
let genResult = gen() let genResult2 = gen() //第一個序列 genResult.next().value 輸出:"first generator" //第二個序列 genResult2.next().value 輸出:"first generator"
10.3.2 yield關鍵詞
來看一個簡單的Generator序列
function* generatorSequence(){ yield 'first'; yield 'second'; yield 'third'; }
創建實例並調用
let genSequence = generatorSequence(); genSequence.next().value //"first" genSequence.next().value //"second" genSequence.next().value //"third"
yield讓Generator惰性的生成一個值的序列。(直到調用才會執行)
yield使Generator函數暫停了執行並將結果返回給調用者,並且它還准確地記住了暫停的位置。下一次調用時就從中斷的地方恢復執行。
10.2.4 done屬性
done是一個判斷Generator序列已經被完全消費的屬性。當done為true時就應該停止調用Generator實例的next。
let genSequence = generatorSequence(); genSequence.next() //{value: "first", done: false} genSequence.next() //{value: "second", done: false} genSequence.next() //{value: "third", done: false} genSequence.next() //{value: undefined, done: true}
下面的for...of循環用於遍歷Generator
function* generatorSequence(){ yield 'first'; yield 'second'; yield 'third'; } for(let value of generatorSequence()){ console.log(value) //first second third }
10.2.5 向Generator傳遞數據
function* sayFullName(){ var firstName = yield; var secondName = yield; console.log(firstName+secondName) } let fullName = sayFullName(); fullName.next() fullName.next('xiao ') fullName.next('ming') 輸出:xiao ming
分析:第一次調用fullName.next()
時,代碼將返回並暫停於var firstName = yield;
第二次調用yield
被'xiao '替換,暫停在var secondName = yield;
,第三次調用yield被'ming'替換,不再有yield。
10.3使用Generator處理異步調用
簡單的異步函數
let getDataOne = (cb) => { setTimeout(function(){ //調用函數 cb('dummy data one') },1000) } let getDataTwo = (cb) => { setTimeout(function(){ //調用函數 cb('dummy data two') },1000) }
調用
getDataOne((data)=>console.log(data)) //1000毫秒之后打印dummy data one getDataTwo((data)=>console.log(data)) //1000毫秒之后打印dummy data two
下面改造getDataOne和getDataTwo函數,使其使用Generator實例而不是回調來傳送數據
let generator; let getDataOne = () => { setTimeout(function(){ //調用Generator,通過next傳遞數據 generator.next('dummy data one') },1000) } let getDataTwo = () => { setTimeout(function(){ //調用Generator,通過next傳遞數據 generator.next('dummy data two') },1000) }
將getDataOne和getDataTwo調用封裝到一個單獨的Generator函數中
function* main(){ let dataOne = yield getDataOne(); let dataTwo = yield getDataTwo(); console.log(dataOne) console.log(dataTwo) }
用之前聲明的generator變量為main創建一個Generator實例。該Generator實例被getDataOne和getDataTwo同時用於向其調用傳遞數據。generator.next()
用於觸發整個過程。main 函數開始執行,並遇到了第一個yield:let dataOne = yield getDataOne();
generator = main()
generator.next()
console.log("first be printed") 輸出: first be printed 1000毫秒之后打印 dummy data one dummy data two
main代碼看上去是在同步的調用getDataOne和getDataTwo,但其實兩個調用都是異步的。
有一點需要注意:雖然yield使語句暫停了,但它不會讓調用者阻塞。
generator.next() //雖然Generator為異步代碼暫停了 console.log("first be printed") //console.log正常執行,說明generator.next不會阻塞執行