1. 程式人生 > >函數語言程式設計之陣列的函數語言程式設計

函數語言程式設計之陣列的函數語言程式設計

5. 陣列的函數語言程式設計

在本章中,我們將建立一組用於陣列的函式,並用函式式的方法而非命令式的方法來解決常見的問題

5.1 陣列的函式式方法

本節將建立一組有用的函式,並用它們解決陣列的常見問題

本節所建立的所有函式稱為投影函式,把函式應用於一個值並建立一個新值的過程稱為投影。講個通俗的例子,forEach 沒有返回值,所以就不是投影函式,map 有返回值,所以是投影函式

5.1.1 map

之前我們已經簡單實現過 forEach,如下

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

map 的程式碼實現如下

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

map 的實現和 forEach 非常相似,區別只是用了一個新的陣列來捕獲了結果,並從函式中返回了結果。

下面使用 map 函式來解決把陣列內容平方的問題

map([1, 2, 3],(x) => x * x );
// [1, 4, 9]

如上所示,我們簡單而優雅的完成了任務,由於要建立很多特別的陣列函式,我們把所有的函式封裝到一個名為 arrayUtils 的常量中並匯出

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

const arrayUtils = {
    map:map,
}

export {arrayUtils}

// 另一個檔案
import arrayUtils form 'lib'
arrayUtils.map // 使用 map

// 或者
const map = arrayUtils.map
// 如此可以直接呼叫 map

為了讓本章的例子更具有實用性,我們要構建一個物件陣列,如下

let apressBooks = [
    {
        'id': 111,
        'title': 'c# 6.0',
        'author': 'Andrew Troelsen',
        'rating': [4.7],
        'reviews': [{good: 4, excellent: 12}]
    },
    {
        'id': 222,
        'title': 'Efficient Learning Machines',
        'author': 'Rahul Khanna',
        'rating': [4.5],
        'reviews': []
    },
    {
        'id': 333,
        'title': 'Pro AngularJS',
        'author': 'Adam Freeman',
        'rating': [4.0],
        'reviews': []
    },
    {
        'id': 444,
        'title': 'Pro ASP.NET',
        'author': 'Adam Freeman',
        'rating': [4.2],
        'reviews': [{good: 14, excellent: 12}]
    },
]

本章建立的所有函式都會基於該物件陣列執行。假設需要獲取它,但只需要包含 title 和 author 欄位。如何通過 map 函式完成?非常簡單

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

這將返回期望的結果,返回的陣列中的物件只會包含 title 和 author 屬性

[
    {title: "c# 6.0", author: "Andrew Troelsen"},
    {title: "Efficient Learning Machines", author: "Rahul Khanna"},
    {title: "Pro AngularJS", author: "Adam Freeman"},
    {title: "Pro ASP.NET", author: "Adam Freeman"}
]

有時候我們並不總是隻想把所有的陣列內容轉換成一個新陣列,還想過濾陣列的內容,然後再做轉換,下面介紹一個名為 filter 的函式

5.1.2 filter

假設我們只想獲取評級高於 4.5 的圖書列表,該如何做?這顯然不是 map 能解決的,我們需要一個類似 map 的函式,但是把結果放入陣列前判斷是否滿足條件

我們可以在 map 函式將結果放入陣列前加入一個條件

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

有了 filter 函式我們就可以以如下方式解決問題了

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

這將返回我們期望的結果

[
    {
        'id': 111,
        'title': 'c# 6.0',
        'author': 'Andrew Troelsen',
        'rating': [4.7],
        'reviews': [{good: 4, excellent: 12}]
    }
]

至此,我們在不斷使用高階函式改進處理陣列的方式,再繼續介紹下一個陣列函式之前,我們將瞭解如何連線投影函式(map,filter),以便能在複雜的環境下獲得期望的結果。

5.2 連線操作

為了達成目標,我們經常需要連線很多函式,例如,從 apressBooks 中獲取含有 title 和 author 物件,且評級高於 4.5 的物件。首先,我們用之前的 map 和 filter 來做

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

此處要注意的是,map 和 filter 都是投影函式,因此它們總是對陣列應用轉換操作後再返回資料,於是我們能夠連線 filter 和 map 來完成任務

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

上面程式碼描述了我們正在解決的問題:map 基於過濾後的陣列(評級高於 4.5)返回了帶有 title 和 author 欄位的物件!

由於 map 和 filter 的特性,我們抽象出了陣列的細節並專注於問題本身。

本章後面將通過函式組合完成同樣的事

5.2.1 concatAll

下面對 apressBooks 物件稍作修改

let apressBooks = [
    {
        name: 'beginers',
        bookDetails:[
            {
                'id': 111,
                'title': 'c# 6.0',
                'author': 'Andrew Troelsen',
                'rating': [4.7],
                'reviews': [{good: 4, excellent: 12}]
            },
            {
                'id': 222,
                'title': 'Efficient Learning Machines',
                'author': 'Rahul Khanna',
                'rating': [4.5],
                'reviews': []
            }
        ]
    },
    {
        name: 'pro',
        bookDetails:[
            {
                'id': 333,
                'title': 'Pro AngularJS',
                'author': 'Adam Freeman',
                'rating': [4.0],
                'reviews': []
            },
            {
                'id': 444,
                'title': 'Pro ASP.NET',
                'author': 'Adam Freeman',
                'rating': [4.2],
                'reviews': [{good: 14, excellent: 12}]
            }
        ]
    }
]

現在讓我回顧上一個問題,獲取含有 title 和 author 物件,且評級高於 4.5 的物件。首先使用 map 函式

map(apressBooks,book => book.bookDetails)
// 返回
[
    [
        {
            'id': 111,
            'title': 'c# 6.0',
            'author': 'Andrew Troelsen',
            'rating': [4.7],
            'reviews': [{good: 4, excellent: 12}]
        },
        {
            'id': 222,
            'title': 'Efficient Learning Machines',
            'author': 'Rahul Khanna',
            'rating': [4.5],
            'reviews': []
        }
    ],
    [
        {
            'id': 333,
            'title': 'Pro AngularJS',
            'author': 'Adam Freeman',
            'rating': [4.0],
            'reviews': []
        },
        {
            'id': 444,
            'title': 'Pro ASP.NET',
            'author': 'Adam Freeman',
            'rating': [4.2],
            'reviews': [{good: 14, excellent: 12}]
        }
    ]
]

如你所見,map 函式返回的資料包含了陣列中的陣列,因為 bookDetails 本身就是一個數組,如果把上面的資料傳給 filter,我們將遇到問題,因為 filter 不能在巢狀的陣列上執行,這就是 concatAll 函式發揮作用的地方

concatAll 函式就是把所有巢狀陣列連線到一個數組中,也可以說是陣列的扁平化(flatten)方法。實現如下

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

concatAll 的主要目的是將巢狀的陣列轉換成非巢狀的單一陣列,下面的程式碼說明了這個概念

concatAll( map(apressBooks,book => book.bookDetails) )
// 返回
[
    {
        'id': 111,
        'title': 'c# 6.0',
        'author': 'Andrew Troelsen',
        'rating': [4.7],
        'reviews': [{good: 4, excellent: 12}]
    },
    {
        'id': 222,
        'title': 'Efficient Learning Machines',
        'author': 'Rahul Khanna',
        'rating': [4.5],
        'reviews': []
    },
    {
        'id': 333,
        'title': 'Pro AngularJS',
        'author': 'Adam Freeman',
        'rating': [4.0],
        'reviews': []
    },
    {
        'id': 444,
        'title': 'Pro ASP.NET',
        'author': 'Adam Freeman',
        'rating': [4.2],
        'reviews': [{good: 14, excellent: 12}]
    },
]

現在就能繼續使用 filter 了

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

可以看到,設計陣列的高階函式可以優雅的解決很多問題

5.3 reduce 函式

reduce 函式大家應該都不陌生,比如求一個數組所有數字的和

[1,2,3,4,5].reduce((pre,cur) => pre+cur);

現在讓我們自己實現一下

const reduce = (array,fn) => {
    let accumlator = 0; // 累加器
    for(const value of array){
        accumlator = fn(accumlator,value)
    }
    return [accumlator];
}
// 使用方法
reduce([1,2,3,4,5],(acc,val) => acc+val)
// [15]

太棒了,但是如果我們要執行乘法呢?那麼 reduce 就會執行失敗,主要在於累加器初始值為 0,所以結果就是 0。

我們可以重寫 reduce 函式來解決該問題,它接受一個為累加器設定初始值的引數

const reduce = (array,fn,initialValue) => {
    let accumlator;
    if(initialValue != undefined){
        accumlator = initialValue;
    }else{
        accumlator = array[0];
    }
    if(initialValue === undefined){
        for(let i = 1; i < array.length; i++){
            accumlator = fn(accumlator,array[i])
        }
    }else{
        for(const value of array){
            accumlator = fn(accumlator,value)
        }
    }
    return [accumlator];
}

我們對 reduce 函式做了修改,如果沒有傳遞初始值,則以陣列的第一個元素作為累加器的值。

現在我們嘗試通過 reduce 函式解決乘積問題

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

現在我們要在 apressBooks 中使用 reduce。

假設有一天老闆讓你實現此邏輯:從 apressBooks 中統計評價為 good 和 excellent 的數量 。你想到,該問題正好可以用 reduce 函式輕鬆解決,我們需要先用 concatAll 將它扁平化,使用 map 取出 bookDetails 並用 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].excellent:0
    return {good:acc.good + goodReviews,excellent:acc.excellent+excellentReviews}
},{good:0,excellent:0})
// 結果
// [{ good: 18, excellent: 24}]

我們把內部細節抽象到了高階函式裡面,產生了優雅的程式碼!

5.4 zip 陣列

有的時候,後臺返回的資料可能是分開的,例如

let apressBooks = [
    {
        name: 'beginers',
        bookDetails:[
            {
                'id': 111,
                'title': 'c# 6.0',
                'author': 'Andrew Troelsen',
                'rating': [4.7],
            },
            {
                'id': 222,
                'title': 'Efficient Learning Machines',
                'author': 'Rahul Khanna',
                'rating': [4.5],
            }
        ]
    },
    {
        name: 'pro',
        bookDetails:[
            {
                'id': 333,
                'title': 'Pro AngularJS',
                'author': 'Adam Freeman',
                'rating': [4.0],
            },
            {
                'id': 444,
                'title': 'Pro ASP.NET',
                'author': 'Adam Freeman',
                'rating': [4.2],
            }
        ]
    }
]
// reviewDetails 物件包含了圖書的評價詳情
let reviewDetails = [
    {
        'id': 111,
        'reviews': [{good: 4, excellent: 12}]
    },
    {
        'id': 222,
        'reviews': []
    },
    {
        'id': 333,
        'reviews': []
    },
    {
        'id': 444,
        'reviews': [{good: 14, excellent: 12}]
    }
]

這個例子中,review 被填充到一個單獨的陣列中,它們與書的 id 相匹配。這是資料被分離到不同部分的典型例子,那麼該如何處理這些分割的資料呢?

zip 函式的任務是合併兩個給定的陣列,就這個例子而言,需要把 apressBooks 和 reviewDetails 合併到一個數組中,如此就能在單一的樹下獲取所有必須的資料,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 函式非常簡單,我們只需要遍歷兩個給定的陣列,由於我們要處理這兩個陣列,所以需要獲取它們的最小長度,然後使用當前的 leftArr 和 rightArr 值呼叫傳入的高階函式 fn。

假設我們要把兩個陣列的內容相加,可以用如下方式使用 zip

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

現在讓我們解決之前的問題

let bookDetails = concatAll(
    map(apressBooks,book => {
        return book.bookDetails
    })
)
let mergedBookDetails = zip(bookDetails,reviewDetails,(book,review)=>{
    if(book.id === review.id){
        let clone = Object.assign({},book)
        clone.ratings = review
        return clone
    }
})

做 zip 操作時,我們接受 bookDetails 陣列和 reviewDetails 陣列。檢查兩個陣列圓的的 id 是否匹配,如果是,就從 book 中克隆出一個新的物件 clone,然後我們為它增加了 ratings 屬性,並把 review 物件作為其值,最後,我們把 clone 物件返回。

zip 是一個小巧而簡單的函式,但是它的作用非常強大

5.5 小結

今天我們又建立了一些有用的函式如 map,filter,concatAll,reduce 和 zip,讓陣列的操作更加容易,我們把這些函式稱為投影函式,因為它們總是在應用轉換操作後返回陣列。

明天我們將學習函數語言程式設計中一個非常重要的概念:函式柯里化。see you tomorrow