Javscript 高階函式(上)

Vincent Ko發表於2018-07-28

對於ES6的語法的引入,很多時候是我所不理解的,甚至是知道用法(api),卻不清楚何使用、為什麼用。一次偶然的機會,在Youtube看到了一個系列教程,覺得非常不錯,遂記筆記,進行詳細的梳理和整合,非常推薦大家去看原版啦,老師是個10年的coder,講課比小品還生動的(@fun fun function)

因為一次可能更新不完,之後有新的內容會更新在github,歡迎小夥伴star,共同進步.傳送門

filter

從一段程式開始

var tripe = function(x) {
    return x*3;
}

var waffle = tripe;
waffle(3);
複製程式碼

這是一段簡單到不能再簡單的程式碼了,但是,這也是JS的特殊之處。一切皆物件,函式也是物件,函式可以是一個變數(var)。

正式這樣的特殊性,讓函式可以作為另一個函式的引數(callback)進行傳遞,這種方式就是高階函式--函式中包含著另一個函式。下面就從filter函式舉例來說

現在有一個需求,要找到一個陣列中比3大的數字,並拿出來。

先看傳統的,不用filter的方案

function big(arr) {
    let ret = [];
    for(let i = 0; i<arr.length; i++) {
        if arr[i] > 3
        ret.push(arr[i]);
    }
    return ret;
}
// 如果現在需求改成找比3小的?再寫程式碼?而且沒有複用性。
複製程式碼

好了,有沒有可能簡化或者優化下程式碼呢?讓它更具有可維護性,更清楚點? 來看看最簡單的函數語言程式設計,使用filter的例子。

var arr = [1,2,3,4];
var b = arr.filter((x)=>{return x >3 });
console.log(b)  // [4]
複製程式碼

好了,可以看到filter接收一個函式作為引數,函式會遍歷陣列每一個item,當returntrue時,會返回當前的item

這只是一個簡單的邏輯,我們說的函數語言程式設計,目的是為了讓函式儘量功能簡單、邏輯清晰,沒有耦合的組織可以讓函式複用和程式碼高效上更有利。所以我們改寫下上面的內容

var isBig = function(x) {
    return x > 3;
}
var b = arr.filter(isBig);
複製程式碼

這樣子,就完成了解耦,我們就是把函式當作一個變數、引數傳給了另一個函式,這就是最基本的思想。 這樣做的好處是什麼?比如我們現在需要找到比3小的數字,只需要一行程式碼

var c = arr.reject(isBig);
複製程式碼

是的,就是這麼簡單,是不是很神奇? 想想傳統的思路,找比3大和比3小是怎麼做的?

map

map 是對映的意思,其實和filter很相似,只不過map是對映和變換原陣列。什麼意思?看下面的需求

一個陣列中包含了很多物件,我想要獲取這些物件的名字

var animals = [
    {name:'cindy', species:'dog'},
    {name:'hash', species:'duck'},
    {name:'gigi', species:'rabit'},
    {name:'chik', species:'cat'},
]
複製程式碼

好了,傳統的做法又是要迴圈、遍歷然後push之後,返回了。用map就很簡單

var name = animals.map(obj=>{
    return obj.name;
})
複製程式碼

注意,這就是map和filter的不同: filter遍歷,並根據trueflase決定是否返回原物件(item); map就是純粹的變換(transform)了。

當然,我們可以利用map,讓內容變的更加豐富

var name = animals.map(obj=>{
    return `${obj.name}是一個${obj.species}`
})
複製程式碼

reduce

reduce最基本的用法如下:

var arr = [1,2,3,4,5];
var sum = arr.reduce((sum,next)=>{
    return sum + next;
},0)
複製程式碼

reduce接收兩個引數,第一個是callback,第二個引數是初始值arr.reduce(callback(p1,p2),init)

為什麼要初始值呢?注意一下回撥函式中,是兩個引數,p1引數是上一次迴圈(loop)的返回結果,p2引數是當前遍歷到的(item)。 很顯然,那麼第一次遍歷時的p1,就是由init提供的。

reduce絕不僅僅是遍歷一個陣列或者做一個加法這麼簡單。他可以做更復雜事情,可以對物件進行操作。 比如我們看到這個例子:

var name = [
    'sam\tblender\t200\t1    ',
    'sam\tpot\t130\t5    ',
    'nacy\tconaver\t20\t3    ',
    'amy\tpot\t130\t2    ',
    'amy\tblender\t4\t2    '
]
複製程式碼

現在需要統計這個陣列中,sam\nacy\amy的個人財產和資料。 怎麼辦呢? 利用上面介紹的方法組合,可以很好的做到

思路:

  1. map對每個字串進行處理和變換
  2. 使用reduce進行統計
var output = 
    name.map((item)=>{item.trim().split('\t')})
        .reduce((custormers,line)=>{
           custormers[line[0]] = custormers[line[0]] || [];
           custormers.push({
               name: line[0],
               property: line[1],
               price:line[2]
               quilty: line[3]
           })
        },{})

複製程式碼

嗯,可能有點複雜了,一行一行來看。

map的作用: 經過map,將原來每一項的字串,轉換成了陣列,現在是
這樣的形式:
[
    [[sam],[blender],[200],[1]],
    [[sam],[pot],[130],[5]],
    ...
]

好了,繼續就到了reduce。我們給reduce穿的`init`是一個空物件,也就是
會建立一個物件作為返回值
第一次迴圈: 
  custormers:{}  line: [[sam],[blender],[200],[1]]
  custormers.sam = [];
  custormers.sam.push({...})
     ==> 最終 
     custormers = {
        sam:[{
            name:sam,
            property:blender,
            price:200,
            quilty:1
        }]
     }

第二次迴圈:
    custormers:{sam:[...]}  line:[[sam],[pot],[130],[5]]
    custormers.sam = [...]
    繼續為sam這個物件,增加“財產"
    ...

...

複製程式碼

所以看到了,這就是reduce的威力。

reduce 實現compose

好了,reduce的作用可能遠不止於前面介紹的那些,因為至少我們在菜鳥教程上看的話,會很醒目的告訴你,reduce可以實現函式compose

reduce

我擦,這是什麼東西?趕快了解了一下

什麼是compose

compose就是執行一系列的任務(函式),比如有以下任務佇列

let tasks = [step1, step2, step3, step4]
複製程式碼

每一個step都是一個步驟,按照步驟一步一步的執行到結尾,這就是一個compose compose在函數語言程式設計中是一個很重要的工具函式,在這裡實現的compose有三點說明

  • 第一個函式是多元的(接受多個引數),後面的函式都是單元的(接受一個引數)
  • 執行順序的自右向左的
  • 所有函式的執行都是同步的

程式導向的實現

使用遞迴的思想,不斷的檢測任務佇列中是否有任務,如果有任務就執行,並把執行結果傳遞. 實現過程如下

var compose = function(args) {
    let length = args.length;
    let count = lenth - 1;
    let result;
    return function inner(...inArgs) {
        result = args[count].apply(null,inArgs);
        if(coun <= 0) {
            return result;
        } 
        count--;
        return args[count].apply(null,result);
    }
}
複製程式碼

generator實現

generator是會專門寫一個介紹的(就在近期更新),同樣是看了很多內容後,理解了它才做的。 因為generator本身就是實現中斷、處理、再中斷的流程。 generator遇到yield就會丟擲一個iterator,而其next()方法可以傳遞引數,就可以實現傳值,將上一步的執行結果,作為下一步的引數。 好了,下面就generator來實現。

function * iterateSteps(steps) {
    let param;
    steps.forEach((step)=> {
        if(param) {
            param = yield step.apply(null,param)
        } else {
            param = yield;
        }
    })
}

const compose = function(steps) {
    let g = iterateSteps(steps);  //g是一個generator,通過next()執行
    return function(...args) {
        let val = steps.pop().apply(null,args) //val返回的值作為將作為第二個函式的引數
        //第一個函式無法傳參,因此空消耗一個yield
        g.next()
        return steps.reverse.reduce((result,fn)=>{
            g.next(result).value //返回
        },val)
    }
}
複製程式碼

解釋:

這個可能看起來就有點讓人迷糊了。 首先,generator就是一步步來執行函式的,只不過,與普通函式不同,執行到函式的地方,它會把"執行緒"丟擲來,讓外面來進行解決。當下次迭代到next(val)的時候,會把val值傳回。

首先,因為第一次的執行無法傳引數,所以我們將param定義,但是不賦值。 在if-else中,第一次將走到else,執行param = yield。 所以,當第一次執行next的時候,實際只是為了消耗yield。

在compose函式中,首先執行iterataSteps,接下來使用reduce來自動執行 這個generator,就可以使實現compose了。注意這裡Steps.pop.apply(null,args)是彈出了最後一個函式,並執行,拿到的值,作為reduceinit引數。

總結

reduce實現compose的意思,通過上面的函式,已經解釋到了。其實,compose的思路還有Promise、函式交織等思路,這裡不做詳細介紹了,可以在這裡參考

傳送門--實現compose的五種思路

相關文章