函數語言程式設計之陣列的函數語言程式設計
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