淺談 JS ES6函數式編程

第一章:

函數式編程主要基於數學函數和它的思想。javascript

1.1 函數與js方法:

函數是一段能夠經過其名稱被調用的代碼,能夠傳遞參數並返回值。
方法是一段必須經過其名稱及其關聯對象的名稱被調用的代碼。php

//函數 var func = (a)=>{return a} func(5) //用其名稱調用 //方法 var obj = {simple:(a)=>{return a}} obj.simple(5) //用其名稱及其關聯對象調用 

1.2 引用透明性

全部函數對於相同的輸入都將返回相同的值(函數只依賴參數的輸入,不依賴於其餘全局數據,即函數內部沒有全局引用),這使並行代碼和緩存(用值直接替換函數的結果)成爲可能。css

1.3 命令式、聲明式與抽象

命令式主張告訴編譯器「如何」作html

var array = [1,2,3] for(let i=0; o<array.length;i++){ console.log(array[i]) } 

聲明式告訴編譯器「作什麼」,如何作的部分(得到數組長度,循環遍歷每一項)被抽象到高階函數中,forEach就是這樣一個內置函數,本書中咱們都將建立這樣的內置函數。java

var array = [1,2,3] array.forEach(elememt=>console.log(elememt)) 

1.4 函數式編程的好處&純函數

好處就是編寫純函數。純函數是對相同輸入返回相同輸出的函數,不依賴(包含)任何外部變量,因此也不會產生改變外部環境變量的反作用。web

1.5 並行代碼

純函數容許咱們並行執行代碼,由於純函數不會改變它的環境,因此不須要擔憂同步問題。固然,js並無真正的多線程支持並行,但若是你的項目使用了webworker來模擬多線程並行執行任務,這種時候就須要用純函數來代替非純函數。編程

let global = 'something'; let function1 = (input) => { global = "somethingElse" } let function2 = ()=>{ if(global === 'something'){ //業務邏輯 } } 

若是咱們要兩個線程並行執行function1和function2,因爲兩個函數都依賴全局變量global,並行執行就會引發不良的影響(兩個函數的執行順序不一樣會有不一樣的結果),如今把它們改成純函數。c#

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

能夠看到,箭頭函數的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) 
 

 

sort是改變原數組的方法,friends按照age升序排列

若是要比較不一樣的屬性,咱們須要重複編寫比較代碼。下面新建一個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 柯里化用例

假設咱們要編寫一個建立列表的函數,建立列表tableOf二、tableOf三、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"] 

pipe

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不會阻塞執行
相關文章
相關標籤/搜索