JS ES6函數(shù)式編程

第一章:

函數(shù)式編程主要基于數(shù)學(xué)函數(shù)和它的思想。

1.1 函數(shù)與js方法:

函數(shù)是一段可以通過(guò)其名稱(chēng)被調(diào)用的代碼,可以傳遞參數(shù)并返回值。
方法是一段必須通過(guò)其名稱(chēng)及其關(guān)聯(lián)對(duì)象的名稱(chēng)被調(diào)用的代碼。

//函數(shù)
var func = (a)=>{return a}
func(5) //用其名稱(chēng)調(diào)用
//方法
var obj = {simple:(a)=>{return a}}
obj.simple(5) //用其名稱(chēng)及其關(guān)聯(lián)對(duì)象調(diào)用

1.2 引用透明性

所有函數(shù)對(duì)于相同的輸入都將返回相同的值(函數(shù)只依賴(lài)參數(shù)的輸入,不依賴(lài)于其他全局?jǐn)?shù)據(jù),即函數(shù)內(nèi)部沒(méi)有全局引用),這使并行代碼和緩存(用值直接替換函數(shù)的結(jié)果)成為可能。

1.3 命令式、聲明式與抽象

命令式主張告訴編譯器“如何”做

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

聲明式告訴編譯器“做什么”,如何做的部分(獲得數(shù)組長(zhǎng)度,循環(huán)遍歷每一項(xiàng))被抽象到高階函數(shù)中,forEach就是這樣一個(gè)內(nèi)置函數(shù),本書(shū)中我們都將創(chuàng)建這樣的內(nèi)置函數(shù)。

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

1.4 函數(shù)式編程的好處&純函數(shù)

好處就是編寫(xiě)純函數(shù)。純函數(shù)是對(duì)相同輸入返回相同輸出的函數(shù),不依賴(lài)(包含)任何外部變量,所以也不會(huì)產(chǎn)生改變外部環(huán)境變量的副作用。

1.5 并行代碼

純函數(shù)允許我們并行執(zhí)行代碼,因?yàn)榧兒瘮?shù)不會(huì)改變它的環(huán)境,所以不需要擔(dān)心同步問(wèn)題。當(dāng)然,js并沒(méi)有真正的多線程支持并行,但如果你的項(xiàng)目使用了webworker來(lái)模擬多線程并行執(zhí)行任務(wù),這種時(shí)候就需要用純函數(shù)來(lái)代替非純函數(shù)。

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

如果我們要兩個(gè)線程并行執(zhí)行function1和function2,由于兩個(gè)函數(shù)都依賴(lài)全局變量global,并行執(zhí)行就會(huì)引起不良的影響(兩個(gè)函數(shù)的執(zhí)行順序不同會(huì)有不同的結(jié)果),現(xiàn)在把它們改為純函數(shù)。

let function1 = (input,global)=>{
    global = "somethingElse"
}
let function2 = (global)=>{
    if(global === "something"){
        //業(yè)務(wù)邏輯
    }
}

我們移動(dòng)了global變量,把它作為兩個(gè)函數(shù)的參數(shù),使他們變成純函數(shù)?,F(xiàn)在并行執(zhí)行不會(huì)有任何問(wèn)題,由于函數(shù)不依賴(lài)于外部環(huán)境變量,不必?fù)?dān)心線程的執(zhí)行順序。

1.6 可緩存

根據(jù)純函數(shù)對(duì)于給定輸入總是返回相同的輸出,我們可以緩存函數(shù)的輸出,減少多次的輸入來(lái)反復(fù)調(diào)用函數(shù)。

var longRunningFnBookKeeper = {2:3,4:5...}
longRunningFnBookKeeper.hasOwnProperty(ip)?longRunningFnBookKeeper[ip]:longRunningFunction(ip)

1.7 管道與組合

純函數(shù)應(yīng)該被設(shè)計(jì)為:只做一件事。實(shí)現(xiàn)多個(gè)功能通過(guò)函數(shù)的組合來(lái)實(shí)現(xiàn)。
UNIX/LINUX中,在一個(gè)文件中找到一個(gè)特定的名稱(chēng)并統(tǒng)計(jì)它的出現(xiàn)次數(shù):
cat jsBook | grep -i "composing" | wc
組合不是命令行特有的,它是函數(shù)式編程的核心。

1.8 關(guān)于js

js是一門(mén)面對(duì)對(duì)象的語(yǔ)言,不是一種純函數(shù)語(yǔ)言,更像是一種多范式語(yǔ)言,但是非常適合函數(shù)式編程。

第二章:js函數(shù)基礎(chǔ)

今天很多瀏覽器還不支持ES6,我們可以通過(guò)轉(zhuǎn)換編譯器babel,將ES6轉(zhuǎn)換為ES5代碼。

箭頭函數(shù)的this經(jīng)過(guò)編譯后為undefined

可以看到,箭頭函數(shù)的this經(jīng)過(guò)編譯后為undefined,轉(zhuǎn)換后的代碼運(yùn)行在嚴(yán)格模式下,嚴(yán)格模式是js的受限變體。

"use strict"
a = 1 // -> Uncaught ReferenceError: a is not defined; 此處直接報(bào)錯(cuò)

在函數(shù)內(nèi)部如果用var聲明變量和不用時(shí)有很大差別,用var聲明的是局部變量,在函數(shù)外部訪問(wèn)這個(gè)變量是訪問(wèn)不到的,沒(méi)var聲明的是全局變量。在函數(shù)外部是可以訪問(wèn)到的。

如果你不使用var命令指定,在全局狀態(tài)下定一個(gè)變量。在嚴(yán)格模式下這段代碼會(huì)報(bào)錯(cuò),因?yàn)槿肿兞吭趈s中非常有害。

第三章:高階函數(shù)

高階函數(shù)(HOC):

  • 接收函數(shù)作為參數(shù)
  • 返回函數(shù)作為輸出
  • 接收函數(shù)作為參數(shù)且返回函數(shù)作為輸出

滿(mǎn)足以上三個(gè)之一的函數(shù)就是高階函數(shù)。

3.1 理解數(shù)據(jù)

3.1.1 js中函數(shù)為一等公民:

因?yàn)楹瘮?shù)也是js中的一種數(shù)據(jù)類(lèi)型,可以被賦值給變量,作為參數(shù)傳遞,也可被其他函數(shù)返回。

3.1.2 把一個(gè)函數(shù)存入變量
let fn = () => {}  //fn就是一個(gè)指向函數(shù)數(shù)據(jù)類(lèi)型的變量,即函數(shù)的引用
fn() //調(diào)用函數(shù),即執(zhí)行fn指向的函數(shù)
3.1.3 函數(shù)作為參數(shù)傳入
var tellType = arg =>{
    if(typeof arg==='function'){
        arg() //如果傳入的是函數(shù)就執(zhí)行
    }else{
        console.log(arg) //否則就輸出數(shù)據(jù)
    }
}
var fn = () => {console.log('i am a function')} 
tellType(fn) //函數(shù)作為參數(shù)傳入
3.1.4 返回函數(shù)

String是js的內(nèi)置函數(shù),注意:只返回了函數(shù)的引用,并沒(méi)有執(zhí)行函數(shù)

let crazy = () =>{ return String } 
crazy() // String() { [native code] }
crazy()('HOC') // "HOC"

3.2 抽象和高階函數(shù)

高階函數(shù)就是定義抽象

3.2.1通過(guò)高階函數(shù)實(shí)現(xiàn)抽象

forEach實(shí)現(xiàn)遍歷數(shù)組

const forEach = (array,fn)=>{
  for(let i=0;i<array.length;i++){
    fn(array[i])
  }
}

forEachObject實(shí)現(xiàn)遍歷對(duì)象

const forEachObject = (obj,fn)=>{
    for(var property in obj){
        if(obj.hasOwnProperty(properity){
            fn(property,obj[property])
        })
    }
}

注意:forEach和forEachObject都是高階函數(shù),他們使開(kāi)發(fā)者專(zhuān)注于任務(wù),而抽象出遍歷的部分。

unless函數(shù):如果predicate為false,則調(diào)用fn

const unless = (predicate,fn)=>{
    if(!predicate)
        fn()
}

查找一個(gè)列表中的偶數(shù)

forEach([1,2,3,4,6,7],(number)=>{
    unless((number%2),()=>{
        console.log(number,"is even")
    })
})

如果我們操作的是一個(gè)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 真實(shí)的高階函數(shù)

a.every(function(element, index, array))
every是所有函數(shù)的每個(gè)回調(diào)函數(shù)都返回true的時(shí)候才會(huì)返回true,當(dāng)遇到false的時(shí)候終止執(zhí)行,返回false。

a.some(function(element, index, array))
some函數(shù)是“存在”有一個(gè)回調(diào)函數(shù)返回true的時(shí)候終止執(zhí)行并返回true,否則返回false
在空數(shù)組上調(diào)用every返回true,some返回false。

3.3.1 every函數(shù)

every函數(shù)接受兩個(gè)參數(shù):一個(gè)數(shù)組和一個(gè)函數(shù)。它使用傳入的函數(shù)檢查數(shù)組的所有元素是否為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循環(huán):ES6中用于遍歷數(shù)組元素的方法,重寫(xiě)every方法

const every = (arr,fn)=>{
    let result = true;
    for(const element of arr){
        result = result&&fn(element)
    }
    return true;
}

3.3.2 some函數(shù)

some函數(shù)接受兩個(gè)參數(shù):一個(gè)數(shù)組和一個(gè)函數(shù)。它使用傳入的函數(shù)檢查數(shù)組的所有元素是否為true, 只要有一個(gè)為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函數(shù)

sort函數(shù)是一個(gè)高階函數(shù),它接受一個(gè)函數(shù)作為參數(shù),該函數(shù)幫助sort函數(shù)決定排序邏輯, 是一個(gè)改變?cè)瓟?shù)組的方法。

arr.sort([compareFunc])

compareFunc是可選的,如果compareFunc未提供,元素將被轉(zhuǎn)換為字符串并按Unicode編碼點(diǎn)順序排列。
compareFunc應(yīng)該實(shí)現(xiàn)下面的邏輯

function compareFunc(a,b){
    if(根據(jù)某種排序標(biāo)準(zhǔn)a<b){
        return -1
    }
    if(根據(jù)某種排序標(biāo)準(zhǔn)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
}    

寫(xiě)成以下也ok,按照age升序排列

function compareFunc(a,b){
    return a.age>b.age
}  
friends.sort(compareFunc)
sort是改變?cè)瓟?shù)組的方法,friends按照age升序排列

如果要比較不同的屬性,我們需要重復(fù)編寫(xiě)比較代碼。下面新建一個(gè)sortBy函數(shù),允許用戶(hù)基于傳入的屬性對(duì)對(duì)象數(shù)組排序。

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函數(shù)接受一個(gè)屬性冰返回另一個(gè)函數(shù),這個(gè)返回的函數(shù)就作為compareFunc傳遞給sort函數(shù),持有property參數(shù)值的返回函數(shù)之所以能夠運(yùn)行是因?yàn)閖s支持閉包。

第四章:高階函數(shù)與閉包

4.1理解閉包

4.1.1什么是閉包

簡(jiǎn)言之,閉包是一個(gè)內(nèi)部函數(shù),它是在一個(gè)函數(shù)內(nèi)部的函數(shù)。

function outer(){
    function inner(){   
    }
}

函數(shù)inner稱(chēng)為閉包函數(shù),閉包如此強(qiáng)大的原因在于它對(duì)作用域鏈的訪問(wèn)。

閉包有3個(gè)可以訪問(wèn)的作用域:
1.閉包函數(shù)內(nèi)聲明的變量
2.對(duì)全局變量的訪問(wèn)
3.對(duì)外部函數(shù)變量的訪問(wèn)?。。?!

let global = 'global';//2
function outer(){
    let outer = 'outer';
    function inner(){
        let a=5;//1
        console.log(outer) //3.閉包能夠訪問(wèn)外部函數(shù)變量
    }
    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

當(dāng)執(zhí)行var closeureFn = fn(5)時(shí),函數(shù)innerFn被返回,js執(zhí)行引擎視innerFn為一個(gè)閉包,并相應(yīng)的設(shè)置了它的作用域。3個(gè)作用域?qū)蛹?jí)在innerFn返回時(shí)都被設(shè)置了。
如此,closeureFn()通過(guò)作用域鏈被調(diào)用時(shí)就記住了arg、outer的值。

我們回到sortBy

const sortBy = (property)=>{
    return (a,b) => {
        return (a[property]<b[property])?-1:(a[property]>b[property])?1:0
    }
}

當(dāng)我們以如下形式調(diào)用時(shí)

sortBy('age')

發(fā)生下面的事情:
sortBy函數(shù)返回了一個(gè)接受兩個(gè)參數(shù)的新函數(shù),這個(gè)新函數(shù)就是一個(gè)閉包
(a,b)=>{/*實(shí)現(xiàn)*/}
根據(jù)閉包能訪問(wèn)作用域?qū)蛹?jí)的特點(diǎn),它能在它的上下文中持有property的值,所以它將在合適并且需要的時(shí)候使用返回值。

4.2真實(shí)的高階函數(shù)

4.2.1 once:允許只運(yùn)行一次給定的函數(shù)

這在開(kāi)發(fā)過(guò)程中很常見(jiàn),例如只想設(shè)置一次第三方庫(kù),初始化一次支付設(shè)置。

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)的含義是執(zhí)行兩個(gè)參數(shù)并返回第二個(gè)表達(dá)式的結(jié)果。
注意:once函數(shù)接受一個(gè)參數(shù)fn并通過(guò)調(diào)用fn的apply方法返回結(jié)果。我們聲明了done變量,返回的函數(shù)會(huì)形成一個(gè)覆蓋它的閉包作用域,檢查done是否為true,如果是則返回undefined,
否則將done設(shè)為true,如此就阻止了下一次的執(zhí)行。

4.2.2 memoized

用于為每一個(gè)輸入存儲(chǔ)結(jié)果,以便于重用函數(shù)中的計(jì)算結(jié)果。

const memoized = (fn) => {
    const lookupTable = {};
    return (arg) => lookupTable[arg] || (lookupTable[arg]=fn(arg));
}

有一個(gè)名為lookupTable的局部變量,它在返回函數(shù)的閉包上下文中。返回函數(shù)將接受一個(gè)參數(shù)并檢查它是否在lookupTable中。
如果在,就返回對(duì)應(yīng)的值,否則使用新的輸入作為key,fn(arg)的結(jié)果為value,更新lookupTable對(duì)象。

求函數(shù)的階乘(遞歸法)

var factorial = (n) => {
    if(n===0){
        return 1;
    }
    return n*factorial(n-1)
}

現(xiàn)在可以改為把factorial函數(shù)包裹進(jìn)一個(gè)memoized函數(shù)來(lái)保留它的輸出(存儲(chǔ)結(jié)果法)

let factorial = memoized((n)=>{
    if(n===0){
        return 1;
    }
    return n*factorial(n-1)
})

它以同樣的方式運(yùn)行,但是比之前快的多。

第五章:數(shù)組的函數(shù)式編程

我們使用數(shù)組來(lái)存儲(chǔ)、操作和查找數(shù)據(jù),以及轉(zhuǎn)換(投影)數(shù)據(jù)格式。本章中使用函數(shù)式編程來(lái)改進(jìn)這些操作。

5.1 數(shù)組的函數(shù)式方法

本節(jié)創(chuàng)建的所有函數(shù)稱(chēng)為投影函數(shù),把函數(shù)應(yīng)用于一個(gè)值并創(chuàng)建一個(gè)新值的過(guò)程稱(chēng)為投影。

5.1.1 map

首先來(lái)看遍歷數(shù)組的forEach方法

const forEach = (array,fn) => {
    for(const value of array)
        fn(value)
}

map函數(shù)的實(shí)現(xiàn)代碼如下

const map = (array,fn) => {
    let results= [];
    for(const value of array)
        results.push(fn(value))
    return results;
}

map和forEach非常類(lèi)似,區(qū)別是用一個(gè)新的數(shù)組捕獲了結(jié)果,并返回了結(jié)果。

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}]
    }
];

假設(shè)只需要獲取包含title和author的字段

map(apressBooks,(book)=>{
    return {title:book.title,author:book.author}
})
5.1.2 filter

有時(shí)我們還想過(guò)濾數(shù)組的內(nèi)容(例如獲取rating>4.5的圖書(shū)列表),再轉(zhuǎn)換為一個(gè)新數(shù)組,因此我們需要一個(gè)類(lèi)似map的函數(shù),它只需要在把結(jié)果放入數(shù)組前檢查一個(gè)條件。

const filter = (array,fn) => {
    let results= [];
    for(const value of array)
        fn(value) ? results.push(value) : undefined
    return results;
}

調(diào)用高階函數(shù)filter

filter(apressBooks, (book)=>book.rating[0]>4.5)

返回結(jié)果


5.2 連接操作

map和filter都是投影函數(shù),因此它們總是對(duì)數(shù)組應(yīng)用轉(zhuǎn)換操作后再返回?cái)?shù)據(jù),于是我們能夠連接filter和map(注意順序)來(lái)完成任務(wù)而不需要額外變量。

例如:從apressBooks中獲取含有title和author對(duì)象且評(píng)級(jí)高于4.5的對(duì)象。

map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{
    return {title:book.title,author:book.author}
})

我們將后面的章節(jié)中國(guó)通過(guò)函數(shù)組合來(lái)完成同樣的事情。

concatAll

對(duì)apressBooks對(duì)象稍作修改,得到如下數(shù)據(jù)結(jié)構(gòu)

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}]
            }
        ]

    }
];

現(xiàn)在回顧上一節(jié)的問(wèn)題:獲取含有title和author字段且評(píng)級(jí)高于4.5的圖書(shū)。

map(apressBooks,(book)=>{
    return book.bookDetails
})

得到如下輸出

如上圖所示,map函數(shù)返回的數(shù)據(jù)包含了數(shù)組中的數(shù)組,如果把上面的數(shù)據(jù)傳給filter將會(huì)遇到問(wèn)題,因?yàn)閒ilter不能在嵌套數(shù)組上運(yùn)行。

我們定義一個(gè)concatAll函數(shù)把所有嵌套數(shù)組連接到一個(gè)數(shù)組中,也可稱(chēng)concatAll為flatten方法(嵌套數(shù)組平鋪)。concatAll的主要目的是將嵌套數(shù)組轉(zhuǎn)換為非嵌套的單一數(shù)組。

const concatAll = (array) => {
    let results = [];
    for(const value of array){
        results.push.apply(results,value)  //重點(diǎn)??!
    }
    return results;
}

使用js的apply方法,將push的上下文設(shè)置為results

concatAll(map(apressBooks,(book)=>{
    return book.bookDetails
}))

返回了我們期望的結(jié)果(數(shù)組平鋪)

轉(zhuǎn)換為非嵌套的單一數(shù)組后就可以繼續(xù)使用filter啦

filter(
    concatAll(map(apressBooks,(book)=>{
    return book.bookDetails
    })),(book) => (book.rating[0] > 4.5)
)

返回結(jié)果

完整代碼

flatten嵌套數(shù)組扁平化
let arr = [[1,2,[3,4]],[4,5],77]

遍歷每一項(xiàng),如果仍是數(shù)組的話就遞歸調(diào)用flatten,并將結(jié)果與result concat一下。如果不是數(shù)組就直接push該項(xiàng)到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函數(shù)

reduce為保持Javascript閉包的能力所設(shè)計(jì)。
先來(lái)看一個(gè)數(shù)組求和問(wèn)題:

let useless = [2,5,6,1,10]

let result = 0;
forEach(useless,value=>{
    result+=value;
})
console.log(result) //24

對(duì)于上面的問(wèn)題,我們將數(shù)組歸約為一個(gè)單一的值,從一個(gè)累加器開(kāi)始(result),在遍歷數(shù)組時(shí)使用它存儲(chǔ)求和結(jié)果。
歸約數(shù)組:設(shè)置累加器并遍歷數(shù)組(記住累加器的上一個(gè)值)以生成一個(gè)單一元素的過(guò)程稱(chēng)為歸約數(shù)組。

我們將這種歸約操作抽象成reduce函數(shù)。

reduce函數(shù)的第一個(gè)實(shí)現(xiàn)

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]

但如果我們要求給定數(shù)組的乘積,reduce函數(shù)會(huì)執(zhí)行失敗,主要是因?yàn)槲覀兪褂昧死奂悠鞯闹?。

我們修改reduce函數(shù),讓它接受一個(gè)為累加器設(shè)置初始值的參數(shù)。
如果沒(méi)有傳遞initialValue時(shí),則以數(shù)組的第一個(gè)元素作為累加器的值。

const reduce = (array,fn,initialValue)=>{
    let accumlator;
    if(initialValue != undefined)
        accumlator = initialValue;
    else
        accumlator = array[0];
    //當(dāng)initialValue未定義時(shí),我們需要從第二個(gè)元素開(kāi)始循環(huán)數(shù)組
    if(initialValue === undefined){
        for(let i=1; i<array.length;i++){
            accumlator = fn(accumlator,array[i])
        }
    }else{//如果initialValue由調(diào)用者傳入,我們就需要遍歷整個(gè)數(shù)組。
        for(const value of array){
            accumlator = fn(accumlator,value);
        }
    }
    return [accumlator]
}

嘗試通過(guò)reduce函數(shù)解決乘積問(wèn)題

let useless = [2,5,6,1,10]
reduce(useless,(acc,val)=>acc*val,1) //[600]

reduce使用舉例
從apressBooks中統(tǒng)計(jì)評(píng)價(jià)為good和excellent的數(shù)量。->使用reduce
由于apressBooks包含數(shù)組中的數(shù)組,先需要使用concatAll把它轉(zhuǎn)化為一個(gè)扁平的數(shù)組。

concatAll(map(apressBooks,(book)=>{
    return book.bookDetails
}))

我們使用reduce解決該問(wèn)題。

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函數(shù)體中,我們獲取good和excellent的評(píng)價(jià)詳情,將其存儲(chǔ)在相應(yīng)的變量中,名為goodReviews和excellentReviews。
完整代碼

5.4 zip數(shù)組

再回顧一下之前數(shù)據(jù)的結(jié)構(gòu),我們?cè)赼pressBooks的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}]
            }
        ]
    }
];

但是有時(shí)候數(shù)據(jù)可能被分離到不同部分了。


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被填充到一個(gè)單獨(dú)的數(shù)組中。

let reviewDetails = [
    {
        "id":111,
        "reviews":[{good:4,excellent:12}]
    },
    {
        "id":222,
        "reviews":[]
    },
    {
        "id":111,
        "reviews":[]
    },
    {
        "id":111,
        "reviews":[{good:4,excellent:12}]
    },
]

zip函數(shù)

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:我們只需要遍歷兩個(gè)給定的數(shù)組,由于要處理兩個(gè)數(shù)組詳情,就需要用 Math.min 獲取它們的最小長(zhǎng)度Math.min(leftArr.length, rightArr.length),一旦獲取了最小長(zhǎng)度,我們就能夠用當(dāng)前的leftArr值和rightArr值調(diào)用傳入的高階函數(shù)fn。
假設(shè)我們要把兩個(gè)數(shù)組的內(nèi)容相加,可以采用如下方式使用zip

zip([1,2,3],[4,5,6],(x,y)=>x+y)

繼續(xù)解決上一節(jié)的問(wèn)題:統(tǒng)計(jì)Apress出版物評(píng)價(jià)為good和excellent的總數(shù)。
我們接受bookDetails和reviewDetails數(shù)組,檢查兩個(gè)數(shù)組元素的id是否匹配,如果是,就從book中克隆出一個(gè)新的對(duì)象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添加一個(gè)ratings屬性,以review對(duì)象作為其值
        return clone
    }
})

注意:Object.assign(target, ...sources)
Object.assign() 方法用于將所有可枚舉屬性的值從一個(gè)或多個(gè)源對(duì)象復(fù)制到目標(biāo)對(duì)象。它將返回目標(biāo)對(duì)象。
let clone = Object.assign({},book)clone得到了一份book 對(duì)象的副本,clone指向了一個(gè)獨(dú)立的引用,為clone添加屬性或操作不會(huì)改變真實(shí)的book引用。

完整代碼-請(qǐng)copy到瀏覽器控制臺(tái)運(yùn)行

第六章:柯里化與偏應(yīng)用

6 一些術(shù)語(yǔ)

6.1 一元函數(shù)

只接受一個(gè)參數(shù)的函數(shù)稱(chēng)為一元(unary)函數(shù)。

const identity = (x) => x
6.2 二元函數(shù)

接受兩個(gè)參數(shù)的函數(shù)稱(chēng)為二元(binary)函數(shù)。

const add = (x,y) => x+y;
6.1 變參函數(shù)

指函數(shù)接受的參數(shù)數(shù)量是可變的。ES5中我們通過(guò)arguments來(lái)捕獲可變數(shù)量的參數(shù)。

function variadic(a){
    console.log(a)
    console.log(arguments)
}

調(diào)用

variadic(1,2,3)
1
[1,2,3]

ES6中我們使用擴(kuò)展運(yùn)算符,獲得可變參數(shù)

const variadic = (a,...variadic){
    console.log(a)
    console.log(variadic)
}

調(diào)用

variadic(1,2,3)
1
[2, 3]

6.2 柯里化

柯里化:把一個(gè)多參數(shù)函數(shù)轉(zhuǎn)換為一個(gè)嵌套的一元函數(shù)的過(guò)程。
看個(gè)例子,假設(shè)有一個(gè)名為add的函數(shù)

const add = (x,y)=>x+y;

我們會(huì)如此調(diào)用該函數(shù)add(1,1),得到結(jié)果2。下面是add函數(shù)的柯里化版本:

const addCurried = x => y => x+y;

如果我們用一個(gè)單一的參數(shù)調(diào)用addCurried,

addCurried(3)

它返回一個(gè)函數(shù),在其中x值通過(guò)閉包被捕獲,fn = y => 4+y ,因此可以用如下方式調(diào)用addCurried

addCurried(3)(4) //7

類(lèi)似的乘法函數(shù)

const curri = x=>y=>z=>x*y*z;
curri(2)(3)(4) //24

下面展示了如何把該處理過(guò)程轉(zhuǎn)換為一個(gè)名為curry的方法, curry方法將接收到的函數(shù)參數(shù)curry化

const curry = (binaryFn) => {
    return function(firstArg){
        return function(secondArg){
            return binaryFn(firstArg,secondArg)
        }
    }
}

調(diào)用curry函數(shù),curry化add。

const add = (x,y)=>x+y;
let autoCurried = curry(add)
autoCurried(2)(3) //5
6.2.1 柯里化用例

假設(shè)我們要編寫(xiě)一個(gè)創(chuàng)建列表的函數(shù),創(chuàng)建列表tableOf2、tableOf3、tableOf4等。

const tableOf2 = (y) => 2*y;
const tableOf3 = (y) => 3*y;
const tableOf4 = (y) => 4*y;

現(xiàn)在可以把表格的概念概括為一個(gè)單獨(dú)的函數(shù)

const genericTable = (x,y) => x*y

我們將genericTable柯里化,用2填充tableOf2的第一個(gè)參數(shù),用3填充tableOf3的第一個(gè)參數(shù),用4填充tableOf4的第一個(gè)參數(shù)。

const tableOf2 = curry(genericTable)(2);
const tableOf3 = curry(genericTable)(3);
const tableOf4 = curry(genericTable)(4);
6.2.2 完整curry函數(shù)

添加規(guī)則,檢查如果傳入?yún)?shù)不是function,就會(huì)報(bào)錯(cuò)。

let curry = (fn) => {
    if(typeof fn!=='function'){
        throw Error('No function provided')
    }
}

如果有人為柯里化函數(shù)提供了所有的參數(shù),就需要通過(guò)傳遞這些參數(shù)執(zhí)行真正的函數(shù),重點(diǎn)在于返回函數(shù)curriedFn是一個(gè)變參函數(shù)。

let curry = (fn) => {
    if(typeof fn!=='function'){
        throw Error('No function provided')
    }
    return function curriedFn(){ //返回函數(shù)是一個(gè)變參函數(shù)
        return fn(...arguments) 
    }
    //采用如下寫(xiě)法也ok
    // return function curriedFn(...args){
    //  return fn(...args) 
    // }
}

如果我們有一個(gè)名為multiply的函數(shù):

const multiply = (x,y,z) => x*y*z;

可以通過(guò)如下方式調(diào)用,等價(jià)于multiply(1,2,3)

curry(multiply)(1,2,3) //6 

下面回到把多參數(shù)函數(shù)轉(zhuǎn)換為嵌套的一元函數(shù)(柯里化的定義)

let curry = (fn) => {
    if(typeof fn!=='function'){
        throw Error('No function provided')
    }
    return function curriedFn(...args){ //args是一個(gè)數(shù)組
        if(args.length < fn.length){ //檢查...args傳入的參數(shù)長(zhǎng)度是否小于函數(shù)參數(shù)列表的長(zhǎng)度
            return function(){
                args = [...args,...arguments]
                return curriedFn(...args)
            };
        }
        return fn(...args) //不小于,就和之前一樣調(diào)用整個(gè)函數(shù)
    }
}

args.length < fn.length
檢查...args傳入的參數(shù)長(zhǎng)度是否小于函數(shù)參數(shù)列表的長(zhǎng)度,如果是,就進(jìn)入if代碼塊,如果不是就如之前一樣調(diào)用整個(gè)函數(shù)。
args = [...args,...arguments]用來(lái)連接一次傳入的參數(shù),把他們合并進(jìn)args,并遞歸調(diào)用curriedFn。由于我們將所有傳入的參數(shù) 組合并遞歸地調(diào)用,再下一次調(diào)用中將會(huì)遇到某一個(gè)時(shí)刻if(args.length < fn.length)條件失敗,說(shuō)明這時(shí)args存放的參數(shù)列表的長(zhǎng)度和函數(shù)參數(shù)的長(zhǎng)度相等,程序就會(huì)被調(diào)用 fn(...args)。

調(diào)用

const multiply = (x,y,z) => x*y*z;
curry(multiply)(1)(2)(3) //6
6.2.3 日志函數(shù) —— 柯里化的應(yīng)用

開(kāi)發(fā)者在寫(xiě)代碼時(shí)候會(huì)在應(yīng)用的不同階段編寫(xiě)很多日志。我們編寫(xiě)如下日志函數(shù)。

6.3 柯里化實(shí)戰(zhàn)

6.3.1 在數(shù)組內(nèi)容中查找數(shù)字

在數(shù)組中查找數(shù)字,返回包含數(shù)字的數(shù)組內(nèi)容。
無(wú)需柯里化時(shí),我們可以如下實(shí)現(xiàn)。

["js","number1"].filter(function(e){
    return /[0-9]+/.test(e) //["number1"]
})

采用柯里化的filter函數(shù)

let filter = curry((fn,ary)=>{
    return ary.filter(fn)
})

filter(function(str){
  return /[0-9]+/.test(str);
})(["js","number1"]) //["number1"]
6.3.2 求數(shù)組的平方

前幾章中,我們使用map函數(shù)傳入一個(gè)平凡函數(shù)來(lái)解決問(wèn)題,此處可以通過(guò)curry函數(shù)以另一種方式解決該問(wèn)題。

let map = curry(function(f,ary){
    return ary.map(f)
})
map(x=>x*x)([1,2,3])  //[1, 4, 9]

6.4 數(shù)據(jù)流

我們?cè)O(shè)計(jì)的柯里化函數(shù)總在最后接受數(shù)組,這是有意而為之。如果我們希望最后接受的參數(shù)是位于參數(shù)列表的中間某位置呢?curry就幫不了我們了。

6.4.1偏應(yīng)用

偏應(yīng)用:部分地應(yīng)用函數(shù)參數(shù)。有時(shí)填充函數(shù)的前兩個(gè)參數(shù)和最后一個(gè)參數(shù)會(huì)使中間的參數(shù)處于一種未知狀態(tài),這正是偏應(yīng)用發(fā)揮作用的地方,將未知狀態(tài)的參數(shù)填充為undefined,之后填入其他參數(shù)調(diào)用函數(shù)。

setTimeout(()=>console.log("Do X task"),10)
setTimeout(()=>console.log("Do Y task"),10)

我們?yōu)槊恳粋€(gè)setTimeout函數(shù)都傳入了10,我們希望把10作為常量,在代碼中把它隱藏。curry函數(shù)并不能幫我們解決這個(gè)問(wèn)題,原因是curry函數(shù)應(yīng)用參數(shù)列表的順序是從最左到最右。
一個(gè)方案是把setTimeout封裝一下,如此函數(shù)參數(shù)就會(huì)變成最右邊的一個(gè)。

const setTimeoutWrapper = (time,fn)=>{
    setTimeout(fn,time);
}

然后就能通過(guò)curry函數(shù)來(lái)實(shí)現(xiàn)一個(gè)10ms的延遲了

const delayTenMs = curry(setTimeoutWrapper)(10)
delayTenMs(()=>console.log("Do X task"))
delayTenMs(()=>console.log("Do Y task"))

程序?qū)⒁晕覀冃枰姆绞竭\(yùn)行,但問(wèn)題是創(chuàng)建了setTimeoutWrapper這個(gè)封裝器,這是一種開(kāi)銷(xiāo)。

6.4.2 實(shí)現(xiàn)偏函數(shù)(適用于任何含有多個(gè)參數(shù)的函數(shù))
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)
    }
}

使用該偏函數(shù)

let delayTenMs = partial(setTimeout,undefined,10);
delayTenMs(()=>console.log("Do Y task"))

說(shuō)明:
我們調(diào)用
partial(setTimeout,undefined,10);
這將產(chǎn)生
let args = partialArgs = [undefined,10]返回函數(shù)將記住args的值(閉包)
返回函數(shù)非常簡(jiǎn)單,它接受一個(gè)名為fullArguments的參數(shù)。所以傳入()=>console.log("Do Y task")作為參數(shù),
在for循環(huán)中我們執(zhí)行遍歷并為函數(shù)創(chuàng)建必需的參數(shù)數(shù)組

if(args[i]===undefined){
    args[i] = fullArguments[arg++];
}

從i=0開(kāi)始,
返回函數(shù)將記住args的值,返回函數(shù)非常簡(jiǎn)單,它接受一個(gè)名為fullArguments的參數(shù)。所以傳入
fullArguments = [()=>console.log("Do Y task")]
在if循環(huán)內(nèi)
args[0]===undefined=>true
args[0]=()=>console.log("Do Y task")
如此args就變成
[()=>console.log("Do Y task"),10]
可以看出,args指向我們期望的setTimeout函數(shù)調(diào)用所需的數(shù)組,一旦在args中有了必要的參數(shù),就可以通過(guò)fn.apply(null,args)調(diào)用函數(shù)了。

partial應(yīng)用
注意,我們可以將partial應(yīng)用于任何含有多個(gè)參數(shù)的函數(shù),看下面的例子。js中使用JSON.stringify() 方法將一個(gè)JavaScript值(對(duì)象或者數(shù)組)轉(zhuǎn)換為一個(gè) JSON字符串。

JSON.stringify(value[, replacer[, space]])

value:
必需, 要轉(zhuǎn)換的 JavaScript 值(通常為對(duì)象或數(shù)組)。

replacer:
可選。用于轉(zhuǎn)換結(jié)果的函數(shù)或數(shù)組。
如果 replacer 為函數(shù),則 JSON.stringify 將調(diào)用該函數(shù),并傳入每個(gè)成員的鍵和值。使用返回值而不是原始值。如果此函數(shù)返回 undefined,則排除成員。根對(duì)象的鍵是一個(gè)空字符串:""。
如果 replacer 是一個(gè)數(shù)組,則僅轉(zhuǎn)換該數(shù)組中具有鍵值的成員。成員的轉(zhuǎn)換順序與鍵在數(shù)組中的順序一樣。

space:
可選,文本添加縮進(jìn)、空格和換行符,如果 space 是一個(gè)數(shù)字,則返回值文本在每個(gè)級(jí)別縮進(jìn)指定數(shù)目的空格,如果 space 大于 10,則文本縮進(jìn) 10 個(gè)空格。space 也可以使用非數(shù)字,如:\t。

我們調(diào)用下面的函數(shù)做JSON的美化輸出。

let obj = {obj:"bar",bar:"foo"}
JSON.stringify(obj,null,2);
輸出:
"{
  "obj": "bar",
  "bar": "foo"
}"

可以看到stringify調(diào)用的最后兩個(gè)參數(shù)總是相同的“null,2”,我們可以用partial移除樣板代碼

let prettyPrintJson = partial(JSON.stringify, undefined, null, 2)
prettyPrintJson({obj:"bar",bar:"foo"})
輸出:
"{
  "obj": "bar",
  "bar": "foo"
}"

該偏函數(shù)的小bug:
如果我們使用一個(gè)不同的參數(shù)再次調(diào)用prettyPrintJson,它將總是給出第一次調(diào)用的結(jié)果。

prettyPrintJson({obj:"bar",bar:"foo222"})
輸出:總是給出第一次調(diào)用的結(jié)果
"{
  "obj": "bar",
  "bar": "foo"
}"

因?yàn)槲覀兺ㄟ^(guò)參數(shù)替換undefined值的方式修改partialArgs,而數(shù)組傳遞的是引用。

第七章:組合與管道(compose/pipe)

7.1 組合的概念

函數(shù)式組合:將多個(gè)函數(shù)組合在一起以便能構(gòu)建出一個(gè)新函數(shù)。

Unix的理念

1.每個(gè)程序只做好一件事情。
2.每個(gè)程序的輸出應(yīng)該是另一個(gè)尚不可知的程序的輸入。

Unix管道符號(hào)|

使用Unix管道符號(hào)|,就可以將左側(cè)的函數(shù)輸出作為右側(cè)函數(shù)的輸入。
如果想計(jì)算單詞word在給定文本文件中的出現(xiàn)次數(shù),該如何實(shí)現(xiàn)呢?
cat test.txt | grep 'world' | wc
cat用于在控制臺(tái)現(xiàn)實(shí)文本文件的內(nèi)容,它接受一個(gè)參數(shù)(文件位置)
grep在給定的文本中搜索內(nèi)容
wc計(jì)算單詞在給定文本中的數(shù)量

7.2 compose函數(shù)

本節(jié)創(chuàng)建第一個(gè)compose函數(shù),它需要接收一個(gè)函數(shù)的輸出,并將其作為輸入傳遞給另外一個(gè)函數(shù)。

const compose = (a, b)=>(c)=>a(b(c))

compose 接收函數(shù)a 、b作為輸入,并返回一個(gè)接收參數(shù)c的函數(shù)。當(dāng)用c調(diào)用返回函數(shù)時(shí),它將用輸入c調(diào)用函數(shù)b,b的輸出作為a的輸入,這就是compose函數(shù)的定義。
注意:函數(shù)的調(diào)用方向是從右至左的。

7.3 應(yīng)用compose函數(shù)

例子1:對(duì)一個(gè)給定的數(shù)字四舍五入求和。

let data = parseFloat("3.56")
let number = Math.round(data) //4

下面通過(guò)compose函數(shù)解決該問(wèn)題:

const compose = (a, b)=>(c)=>a(b(c))
let number = compose(Math.round, parseFloat)
number("3.56") //4

以上就是函數(shù)式組合,我們將兩個(gè)函數(shù)(Math.round、parseFloat)組合在一起以便能構(gòu)造出一個(gè)新函數(shù),注意:Math.round和parseFloat知道調(diào)用number函數(shù)時(shí)才會(huì)執(zhí)行。

例子2:計(jì)算一個(gè)字符串中單詞的數(shù)量
已有以下兩個(gè)函數(shù):

let splitIntoSpaces = (str) => str.split(" ")
let count = (array) => array.length;

如果想用這兩個(gè)函數(shù)構(gòu)建一個(gè)新函數(shù),計(jì)算一個(gè)字符串中單詞的數(shù)量。

const countWords = compose(count, splitIntoSpaces)

調(diào)用

countWords("hello what's your name") // 4
7.3.1 引入curry和partial

以上的例子中,僅當(dāng)函數(shù)接收一個(gè)參數(shù)時(shí),我們才能將兩個(gè)函數(shù)組合。但還存在多參數(shù)函數(shù)的情況,我們可以通過(guò)curry和partial函數(shù)來(lái)實(shí)現(xiàn)。

5.2中,我們通過(guò)以下寫(xiě)法從apressBooks中獲取含有title和author對(duì)象且評(píng)級(jí)高于4.5的對(duì)象。

map(filter(apressBooks, (book)=>book.rating[0]>4.5),(book)=>{
    return {title:book.title,author:book.author}
})

本節(jié)使用compose函數(shù)將map和filter組合起來(lái)。
compose只能組合接受一個(gè)參數(shù)的函數(shù),但是map和filter都接受兩個(gè)參數(shù)map(array,fn) filter(array,fn)(數(shù)組,操作數(shù)組的函數(shù)),不能直接將他們組合。我們使用partial函數(shù)部分地應(yīng)用map和filter的第二個(gè)參數(shù)。

我們定義了過(guò)濾圖書(shū)的小函數(shù)filterGoodBooks和投影函數(shù)projectTitleAndAuthor

let filterGoodBooks = (book)=>book.rating[0]>4.5;
let projectTitleAndAuthor = (book)=>{return {title:book.title, author:book.author}}

現(xiàn)在使用compose和partial實(shí)現(xiàn)

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解決問(wèn)題,也可以用curry來(lái)做同樣的事情。
提示:顛倒map和filter的參數(shù)順序。

const mapWrap = (fn,array)=>{
  return map(array,fn)
}

curry和compose

7.3.2 組合多個(gè)函數(shù)

當(dāng)前的compose只能組合兩個(gè)給定的函數(shù),我們重寫(xiě)compose函數(shù),使它能組合三個(gè)、四個(gè)、更多函數(shù)。

const compose = (...fns) => 
    (value) => 
        reduce(fns.reverse(), (acc, fn) => fn(acc), value)

reduce用于把數(shù)組歸約為一個(gè)單一的值,例如求給定數(shù)組的元素乘積,累乘器初始值為1

reduce([1,2,3,4],(acc,val)=>acc*val,1) //24

此處通過(guò)fns.reverse()反轉(zhuǎn)函數(shù)數(shù)組,(acc, fn) => fn(acc)以傳入的acc為參數(shù)依次調(diào)用每一個(gè)函數(shù)。累加器的初始值是value變量,它作為函數(shù)的第一個(gè)輸入。

上一節(jié)中,我們組合了一個(gè)函數(shù)用于計(jì)算給定字符串的單詞數(shù)。

let splitIntoSpaces = (str) => str.split(" ")
let count = (array) => array.length;
const countWords = compose(count, splitIntoSpaces)
countWords("hello what's your name") // 4

假設(shè)我們想知道給定字符串的單詞數(shù)是基數(shù)還是偶數(shù),而我們已經(jīng)有如下函數(shù)

let oddOrEven = (ip) => ip%2 == 0 ? "even" : "odd";

通過(guò)compose,將這三個(gè)函數(shù)組合起來(lái)

const oddOrEvenWords = compose(oddOrEven, count, splitIntoSpaces);
oddOrEvenWords("hello what's your name") // ["even"]

oddOrEvenWords完整代碼

7.4 管道/序列

compose的數(shù)據(jù)流是從右至左的,最右側(cè)的函數(shù)會(huì)首先執(zhí)行,將數(shù)據(jù)傳遞給下一個(gè)函數(shù),以此類(lèi)推...最左側(cè)的函數(shù)最后執(zhí)行。
而當(dāng)我們進(jìn)行“|”操作時(shí),Unix命令的數(shù)據(jù)流總是從左至右的,本節(jié)中,我們將實(shí)現(xiàn)pipe,它和compose函數(shù)所做的事情相同,只不過(guò)交換了數(shù)據(jù)流方向

管道/序列(pipeline/sequence):從左至右處理數(shù)據(jù)流的過(guò)程稱(chēng)為管道/序列。

7.4.1 實(shí)現(xiàn)pipe

pipe是compose的復(fù)制品,唯一修改的是數(shù)據(jù)流方向。

const pipe = (...fns) =>
    (value) => 
        reduce(fns, (acc, fn) => fn(acc), value)

此處沒(méi)有像compose一樣調(diào)用fns.reverse(),這意味著我們將按照原有順序執(zhí)行函數(shù)。
調(diào)用pipe函數(shù)。注意,我們改變了調(diào)用順序,先splitIntoSpaces, 中count, 最后oddOrEven。

const oddOrEvenWords = pipe(splitIntoSpaces, count, oddOrEven);
oddOrEvenWords("hello what's your name") // ["even"]

pipe

7.5 組合的優(yōu)勢(shì):結(jié)合律

函數(shù)式組合滿(mǎn)足結(jié)合律:

compose(compose(f,g),h) == compose(f,compose(g,h))

看一下上一節(jié)的例子

//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"]

真正的好處:把函數(shù)組合到各自所需的compose函數(shù)中,

let countWords = compose(count, splitIntoSpaces)
let oddOrEvenWords = compose(oddOrEven, countWords)

or

let countOddOrEven= compose(oddOrEven, count)
let oddOrEvenWords = compose(countOddOrEven, splitIntoSpaces)

第八章:函子

函子:用一種純函數(shù)式的方式進(jìn)行錯(cuò)誤處理。

8.1.1 函子是容器

函子是一個(gè)實(shí)現(xiàn)了map(遍歷每個(gè)對(duì)象值的時(shí)候生成一個(gè)新對(duì)象)的普通對(duì)象(在其他語(yǔ)言中可能是一個(gè)類(lèi))。簡(jiǎn)而言之,函子是一個(gè)持有值的容器,能夠持有任何傳給它值,并允許使用當(dāng)前容器持有的值調(diào)用任何函數(shù)。

創(chuàng)建Container構(gòu)造函數(shù)

const Container = function(val){
    this.value = val;
}

不使用箭頭函數(shù)的原因是箭頭函數(shù)不具備內(nèi)部方法Construct和prototype屬性,所以不能用new來(lái)創(chuàng)建一個(gè)新對(duì)象。

應(yīng)用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]}

我們?yōu)镃ontainer創(chuàng)建一個(gè)of靜態(tài)工具方法,用以代替new關(guān)鍵詞使用

Container.of = function(value){
    return new Container(value)
}

用of方法重寫(xiě)上面的代碼

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 函子實(shí)現(xiàn)了map方法

map方法允許我們使用當(dāng)前Container持有的值調(diào)用任何函數(shù)。
即map函數(shù)從Container中取出值,將傳入的函數(shù)作用于該值,再將結(jié)果放回Container。

Container.prototype.map = function(fn){
    return Container.of(fn(this.value))
}

第十章:使用Generator

Generator是ES6中關(guān)于函數(shù)的新規(guī)范。它不是一種函數(shù)式編程技術(shù),但它是函數(shù)的一部分。

10.1 異步代碼及其問(wèn)題(回調(diào)地獄)

同步VS異步

同步:函數(shù)執(zhí)行時(shí)會(huì)阻塞調(diào)用者,并在執(zhí)行完后返回結(jié)果。
異步:在執(zhí)行時(shí)不會(huì)阻塞調(diào)用者,一旦執(zhí)行完畢就會(huì)返回結(jié)果。
處理Ajax請(qǐng)求時(shí)就是在處理異步調(diào)用。

同步函數(shù)

let sync = () =>{
    //一些操作
    //返回?cái)?shù)據(jù)
}
let sync2 = () =>{
    //一些操作
    //返回?cái)?shù)據(jù)
}
let sync3 = () =>{
    //一些操作
    //返回?cái)?shù)據(jù)
}

同步函數(shù)調(diào)用

result = sync()
result2 = sync2()
result3 = sync3()

異步函數(shù)

let async = (fn)=>{
    //一些異步操作
    //用異步操作調(diào)用回調(diào)
    fn(/*結(jié)果數(shù)據(jù)*/)
}
let async2 = (fn)=>{
    //一些異步操作
    //用異步操作調(diào)用回調(diào)
    fn(/*結(jié)果數(shù)據(jù)*/)
}
let async3 = (fn)=>{
    //一些異步操作
    //用異步操作調(diào)用回調(diào)
    fn(/*結(jié)果數(shù)據(jù)*/)
}

異步函數(shù)調(diào)用

async(function(x){
    async2(function(y){
        async3(function(z){
            ...
        })
    })
})

10.2 Generator基礎(chǔ)

Generator是ES6規(guī)范的一部分,被捆綁在語(yǔ)言層面。

10.2.1創(chuàng)建Generator
function* gen(){
    return 'first generator';
}
gen()

返回一個(gè)Generator原始類(lèi)型的實(shí)例


調(diào)用實(shí)例的next函數(shù),從該Generator實(shí)例中獲取值

gen().next()
輸出:
{value: "first generator", done: true}

gen().next().value
輸出:
"first generator"

10.2.2 Generator的注意事項(xiàng)

一:不能無(wú)限制地調(diào)用next從Generator中取值

let genResult = gen()
//第一次調(diào)用
genResult.next().value
輸出:"first generator"
//第二次調(diào)用
genResult.next().value
輸出:undefined

原因是Generator如同序列,一旦序列中的值被消費(fèi),你就不能再次消費(fèi)它。
本例中,genResult是一個(gè)帶有"first generator"值的序列,第一次調(diào)用next后,我們就已經(jīng)從序列中消費(fèi)了該值。
由于序列已為空,第二次調(diào)用它就會(huì)返回undefined。
為了能夠再次消費(fèi)該序列,方法是創(chuàng)建另一個(gè)Generator實(shí)例

let genResult = gen()
let genResult2 = gen()
//第一個(gè)序列
genResult.next().value
輸出:"first generator"
//第二個(gè)序列
genResult2.next().value
輸出:"first generator"
10.3.2 yield關(guān)鍵詞

來(lái)看一個(gè)簡(jiǎn)單的Generator序列

function* generatorSequence(){
    yield 'first';
    yield 'second';
    yield 'third';
}

創(chuàng)建實(shí)例并調(diào)用

let genSequence = generatorSequence();
genSequence.next().value  //"first"
genSequence.next().value  //"second"
genSequence.next().value  //"third"

yield讓Generator惰性的生成一個(gè)值的序列。(直到調(diào)用才會(huì)執(zhí)行)
yield使Generator函數(shù)暫停了執(zhí)行并將結(jié)果返回給調(diào)用者,并且它還準(zhǔn)確地記住了暫停的位置。下一次調(diào)用時(shí)就從中斷的地方恢復(fù)執(zhí)行。

10.2.4 done屬性

done是一個(gè)判斷Generator序列已經(jīng)被完全消費(fèi)的屬性。當(dāng)done為true時(shí)就應(yīng)該停止調(diào)用Generator實(shí)例的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循環(huán)用于遍歷Generator

function* generatorSequence(){
    yield 'first';
    yield 'second';
    yield 'third';
}
for(let value of generatorSequence()){
    console.log(value) //first second third
}
10.2.5 向Generator傳遞數(shù)據(jù)
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

分析:第一次調(diào)用fullName.next()時(shí),代碼將返回并暫停于var firstName = yield; 第二次調(diào)用yield被'xiao '替換,暫停在var secondName = yield;,第三次調(diào)用yield被'ming'替換,不再有yield。

10.3使用Generator處理異步調(diào)用

簡(jiǎn)單的異步函數(shù)

let getDataOne = (cb) => {
    setTimeout(function(){
        //調(diào)用函數(shù)
        cb('dummy data one')
    },1000)
}
let getDataTwo = (cb) => {
    setTimeout(function(){
        //調(diào)用函數(shù)
        cb('dummy data two')
    },1000)
}

調(diào)用

getDataOne((data)=>console.log(data)) //1000毫秒之后打印dummy data one
getDataTwo((data)=>console.log(data)) //1000毫秒之后打印dummy data two

下面改造getDataOne和getDataTwo函數(shù),使其使用Generator實(shí)例而不是回調(diào)來(lái)傳送數(shù)據(jù)

let generator;
let getDataOne = () => {
    setTimeout(function(){
        //調(diào)用Generator,通過(guò)next傳遞數(shù)據(jù)
        generator.next('dummy data one')
    },1000)
}
let getDataTwo = () => {
    setTimeout(function(){
        //調(diào)用Generator,通過(guò)next傳遞數(shù)據(jù)
        generator.next('dummy data two')
    },1000)
}

將getDataOne和getDataTwo調(diào)用封裝到一個(gè)單獨(dú)的Generator函數(shù)中

function* main(){
    let dataOne = yield getDataOne();
    let dataTwo = yield getDataTwo();
    console.log(dataOne)
    console.log(dataTwo)
}

用之前聲明的generator變量為main創(chuàng)建一個(gè)Generator實(shí)例。該Generator實(shí)例被getDataOne和getDataTwo同時(shí)用于向其調(diào)用傳遞數(shù)據(jù)。generator.next()用于觸發(fā)整個(gè)過(guò)程。main 函數(shù)開(kāi)始執(zhí)行,并遇到了第一個(gè)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代碼看上去是在同步的調(diào)用getDataOne和getDataTwo,但其實(shí)兩個(gè)調(diào)用都是異步的。
有一點(diǎn)需要注意:雖然yield使語(yǔ)句暫停了,但它不會(huì)讓調(diào)用者阻塞。

generator.next() //雖然Generator為異步代碼暫停了
console.log("first be printed")  //console.log正常執(zhí)行,說(shuō)明generator.next不會(huì)阻塞執(zhí)行
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容