前端必學——函數語言程式設計(五)

語言: CN / TW / HK

前文梳理

第一篇

  1. 為什麼要進行函數語言程式設計?——  一切只是為了程式碼更加可讀!!
  2. 開發人員喜歡【顯式】輸入輸出而不是【隱式】輸入輸出 ,要明白何為顯式,何為隱式!!
  3. 一個函式如果可以接受或返回一個甚至多個函式,它被叫做 高階函式 。閉包是最強大的高階函式!!

第二篇

講了重要的兩個概念: 偏函式柯里化

  1. 函式組裝是函數語言程式設計最重要的實現方式!而熟練運用偏函式、柯里化,以及它們的變體,是函式組裝的基礎。
  2. 偏函式表現形式: partial(sum,1,2)(3)
  3. 柯里化表現形式: sum(1)(2)(3)

第三篇

“函式組裝”這一重點:

  1. 再次重申, 函式組裝是函數語言程式設計最重要的實現方式!!
  2. 函式組裝符合  “宣告式程式設計風格” ,即宣告的時候你就知道了它“是什麼”!而不用知道它具體“幹了什麼”(命令式函式風格)!
  3. 比如:當你看到組裝後的函式呼叫是這樣, compose( skipShortWords, unique, words )( text ) ,就知道了它是先將 text 變成 words,然後 unique 去重,然後過濾較短長度的 words。非常清晰!
  4. compose(..) 函式和 partial(..) 函式結合,可以實現豐富多彩的組裝形式!
  5. 封裝抽象成函式是一門技術活!不能不夠,也不宜太過!

第四篇

再細扣了下  “副作用”

  1. 開發人員喜歡顯式輸入輸出而不是隱式輸入輸出,學函數語言程式設計,這句話要深入骨髓的記憶!
  2. 解決副作用的方法有:定義常量、明確 I/O、明確依賴、運用冪等,記得對冪等留個心眼!
  3. 我們喜歡沒有副作用的函式,即純函式!!
  4. 假如一棵樹在森林裡倒下而沒有人在附近聽見,它有沒有發出聲音? ——對於這個問題的理解就是:假如你封裝了一個高階函式,在內部即使有副作用的情況下,外界會知道這個資訊嗎,它還算是純函式嗎?

以上便是我們的簡要回顧!

我們可能還需要更多時間去實踐和體會:

  1. 偏函式  partial(..) 和函式組裝 compose(..) 的變體及應用;
  2. 抽象的能力;
  3. 封裝高階的純函式;

第五篇,咱們將基於實踐,分享最最常見的現象 ——  陣列操作 ,看看它是如體現函數語言程式設計精神!

陣列三劍客

這三劍客是: map(..) filter(..) 和  reduce(..)

map

我們都會用 ES6  map(..) , 它“是什麼”,我們非常清楚!

輕鬆寫一個  map(..) 的使用:

[1,2,3].map(item => item + 1)

但是, map(..) “幹了什麼”,即它的內部是怎樣的,你知道嗎?

我們可以用原生實現一個函式  map(..)

function map(mapperFn,arr) {
    var newList = [];

    for (let id = 0; id < arr.length; id++) {
        newList.push(
            mapperFn( arr[id], id, arr )
        );
    }

    return newList;
}

map(item=>item+1,[1,2,3])

我們把一個  mapperFn(..) 封裝進模擬的  map(..) 函式內,其內部也是 for 迴圈遍歷。

我們還可以用  map(..) 做更多:

比如先將函式放在列表中,然後組合列表中的每一個函式,最後執行它們,像這樣:

var increment = v => ++v;
var decrement = v => --v;
var square = v => v * v;

var double = v => v * 2;

[increment,decrement,square]
.map( fn => compose( fn, double ) )
.map( fn => fn( 3 ) );
// [7,5,36]

細細品一品~

filter

如果說 map(..) 的本質是對映值, filter(..) 的本質是過濾值。如圖示意:

[1,2,3].filter(item => item>2)

手寫一個  filter(..) 函式:

function filter(predicateFn,arr) {
    var newList = [];

    for (let id = 0; id < arr.length; id++) {
        if (predicateFn( arr[id], id, arr )) {
            newList.push( arr[id] );
        }
    }

    return newList;
}

filter(item=>item>2,[1,2,3])

同樣也是將一個函式作為入參,處理同樣傳入的 arr,遍歷過濾得到目標陣列;

reduce

map(..) 和  filter(..) 都會產生新的陣列,而第三種操作(reduce(..))則是典型地將列表中的值合併(或減少)到單個值(非列表)。

[5,10,15].reduce( (product,v) => product * v, 3 );

過程:

  1. 3 * 5 = 15
  2. 15 * 10 = 150
  3. 150 * 15 = 2250

手動實現 reduce 函式相較前兩個,要稍微複雜些:

function reduce(reducerFn,initialValue,arr) {
    var acc, startId;

    if (arguments.length == 3) {
        acc = initialValue;
        startId = 0;
    }
    else if (arr.length > 0) {
        acc = arr[0];
        startId = 1;
    }
    else {
        throw new Error( "Must provide at least one value." );
    }

    for (let id = startId; id < arr.length; id++) {
        acc = reducerFn( acc, arr[id], id, arr );
    }

    return acc;
}

不像  map(..) 和  filter(..) ,對傳入陣列的次序沒有要求。 reduce(..) 明確要採用從左到右的處理方式。

高階操作

基於  map(..) filter(..) 和  reduce(..) ,我們再看些更復雜的操作;

去重

實現:

var unique =
    arr =>
        arr.filter(
            (v,id) =>
                arr.indexOf( v ) == id
        );

unique( [1,4,7,1,3,1,7,9,2,6,4,0,5,3] );

原理是,當從左往右篩選元素時,列表項的 id 位置和 indexOf(..) 找到的位置相等時,表明該列表項第一次出現,在這種情況下,將列表項加入到新陣列中。

當然,去重方式有很多,但是,這種方式的優點是, 它們使用了內建的列表操作,它們能更方便的和其他列表操作鏈式/組合呼叫。

這裡也寫一下 reduce(..) 實現:

var unique =
    arr =>
        arr.reduce(
            (list,v) =>
                list.indexOf( v ) == -1 ?
                    ( list.push( v ), list ) : list
        , [] );

降維

二位陣列轉一維陣列

[ [1, 2, 3], 4, 5, [6, [7, 8]] ] => [ 1, 2, 3, 4, 5, 6, 7, 8 ]

實現:

var flatten =
    arr =>
        arr.reduce(
            (list,v) =>
                list.concat( Array.isArray( v ) ? flatten( v ) : v )
        , [] );

你還可以加一個引數 depth 來指定降維的層數:

var flatten =
    (arr,depth = Infinity) =>
        arr.reduce(
            (list,v) =>
                list.concat(
                    depth > 0 ?
                        (depth > 1 && Array.isArray( v ) ?
                            flatten( v, depth - 1 ) :
                            v
                        ) :
                        [v]
                )
        , [] );

flatten( [[0,1],2,3,[4,[5,6,7],[8,[9,[10,[11,12],13]]]]], 2 );
// [0,1,2,3,4,5,6,7,8,[9,[10,[11,12],13]]]

看到這裡,如果覺得複雜,你可以只把它作為一個庫來呼叫即可。實際上,我們 後續還會專門來介紹各類函數語言程式設計函式庫

融合

仔細體會下,以下給出的三段程式碼,哪段你覺得你更容易看懂?哪一段更符合函數語言程式設計?

// 實現 1
[1,2,3,4,5]
.filter( isOdd )
.map( double )
.reduce( sum, 0 );                    // 18

// 實現 2
reduce(
    map(
        filter( [1,2,3,4,5], isOdd ),
        double
    ),
    sum,
    0
);                                    // 18

// 實現 3
compose(
    partialRight( reduce, sum, 0 ),
    partialRight( map, double ),
    partialRight( filter, isOdd )
)
( [1,2,3,4,5] );                     // 18

在片段 1 和 片段 3 中無法抉擇?

再看一例:

var removeInvalidChars = str => str.replace( /[^\w]*/g, "" );

var upper = str => str.toUpperCase();

var elide = str =>
    str.length > 10 ?
        str.substr( 0, 7 ) + "..." :
        str;

var words = "Mr. Jones isn't responsible for this disaster!"
    .split( /\s/ );

words;
// ["Mr.","Jones","isn't","responsible","for","this","disaster!"]

// 片段 1
words
.map( removeInvalidChars )
.map( upper )
.map( elide );
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]

// 片段 3
words
.map(
    compose( elide, upper, removeInvalidChars )
);
// ["MR","JONES","ISNT","RESPONS...","FOR","THIS","DISASTER"]

重點就是:

我們可以將那三個獨立的相鄰的 map(..) 呼叫步驟看成一個轉換組合。因為它們都是一元函式,並且每一個返回值都是下一個點輸入值。我們可以採用 compose(..) 執行對映功能,並將這個組合函式傳入到單個 map(..) 中呼叫:

所以:片段 3 這種 融合 的技術,是常見的效能優化方式。

階段小結

以上,我們看到了:

三個強大通用的列表操作:

  1. map(..): 轉換列表項的值到新列表;
  2. filter(..): 選擇或過濾掉列表項的值到新陣列;
  3. reduce(..): 合併列表中的值,並且產生一個其他的值(也可能是非列表的值);

這是我們平常用的最多的陣列遍歷方式,但這次我們藉助函數語言程式設計思想把它們升級了!

這些高階操作:unique(..)、flatten(..)、map 融合的思想等(其實還有很多其它高階操作),值得我們去研究、感受體會,最後運用到實踐中去!!

我是掘金安東尼: 一名人氣前端技術博主(文章 100w+ 閱讀量)

終身寫作者(INFP 寫作人格)

堅持與熱愛(簡書打卡 1000 日)

我能陪你一起度過漫長技術歲月嗎(以夢為馬)

覺得不錯,給個點贊和關注吧(這是我最大的動力 )b( ̄▽ ̄)d