函数式编程之高阶函数

3. 高阶函数

昨天我们把 Node 的环境配置好了,还创建了第一个名为 forEach 的函数式编程 API,并且这个函数接收一个函数。允许以函数代替数据传输是非常强大的概念,这种接收函数作为其参数的函数称为高阶函数(Higher-Order-Function),简称 HOC,今天我们将继续创建几个简单的 HOC 添加到类库中。让我们开始吧

3.1 理解数据

每种编程语言都有数据类型,这些数据类型能够存储数据并允许程序作用其中

JavaScript 支持以下几种数据类型

  • Number
  • String
  • Boolean
  • Object
  • Null
  • Undefined
  • Symbol(ES6 新增)

重要的是函数也可以作为 JavaScript 的一种数据类型。由于函数是类似 String 的数据类型,我们就可以传递它并且将它存入一个变量,这与其他数据类型对的操作非常相似。

当一门语言允许函数作为任何其他数据类型使用时,函数被称为一等公民。也就是说,函数可以被赋值给变量,作为参数传递,也可以被其他函数返回。看个例子

// 函数被赋值给变量
var double = (value) => value * 2;

// 函数作为参数传递
var getJSON = (url,callback) =>{
    var xhr = new XMLHttpRequest();
    // ...发起请求
    callback(xhr.resopnseText)
}

// 函数被其他函数返回
var delayAdd = (value) => {
    return function(value){
        return value + 1;
    }
} 
var add = delayAdd(1); // 此处 add 为返回的函数
var num = add(); // 执行 add,返回结果 2

3.2 抽象和高阶函数

现在我们了解了如何创建并执行高阶函数,那么高阶函数有什么用呢?

高阶函数通常用于抽象通用的问题。换句话说,高阶函数就是定义抽象

3.2.1 抽象的定义

什么是抽象?这里引用一段维基百科的定义

在软件工程和计算机科学中,抽象是一种管理计算机系统复杂性的技术。它通过建立一个人与系统进行交互的复杂程度,把更复杂的细节抑制在当前水平之下。程序员应该使用理想的界面(通常定义良好),并且可以添加额外级别的功能,否则处理起来将会很复杂

介绍中还包含了如下的内容

例如,一个编写涉及数值操作代码的程序员可能不会对底层硬件中的数字的表示形式感兴趣(例如不在乎它们是 16 位还是 32 位整数),包括这些细节在哪里屏蔽。可以说,它们被抽象出来了,只留下了简单的数字给程序员处理

简言之,抽象就是让我们专注于预定的目标而无须关心底层的系统概念

3.2.2 通过高阶函数实现抽象

还记得昨天我们的 forEach 函数吗

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

上面的 forEach 函数就抽象出了遍历数组的问题,用户不需要知道是如何遍历的,如此问题就被抽象出来了。

forEach 本质上遍历了整个数组,那么如何遍历一个 JavaScript 对象呢?步骤如下

  • 遍历给定对象所有的 key
  • 识别 key 是否属于本身
  • 如果是,则获取 key 的值

把以上步骤抽象到一个名为 forEachObject 的高阶函数中,如下

// es6-function.js
const forEachObject = (obj,fn) => {
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            // 以 key 和 value 作为参数调用 fn
            fn(key,obj[key])
        }
    }
}

// 调用
let object = {a:1, b:2};
forEachObject(object,(k,v) => console.log(k + ':' + v))
// a:1
// b:2

注意:forEach 和 forEachObject 都是高阶函数,它们让开发者专注于任务,而抽象出遍历的部分!由于这些遍历函数被抽象出来了,就能够彻底的测试它们,于是产生了简洁的的代码库。下面以抽象的方式实现对控制流程的处理

为此,创建一个名为 unless 的函数,接受一个断言(值为 false 或 true)。如果断言为 false,则调用 fn,代码如下

// es6-function.js
const unless = (predicate,fn) => {
    if(!predicate)
        fn()
}

有了这个函数,就可以编写一段代码来查找一个数组中的偶数

forEach([1,2,3,4,5,6,7],(number) => {
    unless((number % 2),()=>{
        console.log(number + ' 是偶数')
    })
})
// 2 是偶数
// 4 是偶数
// 6 是偶数

下面来看另一个名为 times 的高阶函数,它接受一个数字,并根据调用者提供的次数调用传入的函数,代码如下

// es6-functional.js
const times = (times,fn) => {
    for(let i = 0; i < times; i++)
        fn(i)
}

times 与 forEach 函数类似,不同的是我们操作的是一个 number 而不是 array,如果要输出 0 - 99 中的偶数,就可以这么用

times(100,function(n){
    unless(n % 2,function(){
        console.log(n + " 是偶数")
    })
})

我们将上面的代码抽象出循环,条件判断被放在一个简明的高阶函数中。

3.3 真实的高阶函数

上一节我们写了几个简单的高阶函数,这一节我们将了解真实的高阶函数, 并从简单的高阶函数开始,逐步进入复杂的高阶函数,这些函数都是 JavaScript 开发者日常中会用到的。

3.3.1 every 函数

我们经常需要检查数组的内容是否为数字,数组或其他类型,一般情况下,我们编写典型的循环方法来解决这个问题。但是,下面将这些抽象到一个 every 函数中,它接收两个参数:一个数组和一个函数,它使用传入的函数检查数组的所有元素是否为 true,其实就是数组的 every 方法

// es6-functional.js
const every = (arr, fn) => {
    let result = true;
    for(let i = 0,len = arr.length; i < len; i++){
        result = result && fn(arr[i])
    }    
    return result;  
}

在此处,我们简单的遍历传入的数组,并使用当前遍历的数组元素内容调用 fn。注意传入的 fn 需要返回一个布尔值。然后我们用 && 运算确保所有的数组内容遵循 fn 给出的条件。

让我们测试一下,传入一个 NaN 数组,isNaN 作为 fn 传入,他会检查给定的数字是否为 NaN

every([NaN,NaN,NaN], isNaN)
// true
every([NaN,NaN,4], isNaN)
// false

every 函数是一个典型的高阶函数,实现简单而且非常有用。在继续之前,我们需要了解一下 for…of 循环,它是 ES6 规范的一个部分,用于遍历数组元素,下面用 for … of 循环重写 every 函数

// es6-functional.js
const every = (arr, fn) => {
    let result = true;
    for(const value of arr){
        result = result && fn(value)
    }    
    return result;  
}

for…of 也是旧的 for(…) 循环的抽象,通过隐藏索引变量移除了对数组的遍历。我们使用 every 抽象出了 for…of,这就是抽象。如果下一个版本的 JavaScript 改变了 for…of 的使用方式,我们只需在 every 函数中修改,这就是抽象函数最大的好处。

3.3.2 some 函数

与 every 函数类似,还有一个 some 函数,也是数组的一个方法,some 函数的工作方式和 every 相反,只要数组中的任意一个元素通过传入的函数返回 true,some 函数就将返回 true。some 函数也被称为 any 函数。为实现some 函数,我们需要使用 || 而不是 &&,具体代码如下

// es6-functional.js
const some = (arr,fn) => {
    let result = false;
    for(const value of arr){
        result = result||fn(value)
    }
    return result;
}

注意:这里的 some 和 every 函数都是低效的实现。every 应该在遇到第一个不匹配的元素时就停止遍历数组,some 应该在遇到第一个匹配的元素时就停止遍历数组,在这里只是为了理解高阶函数的概念,而不是为了编写高效的代码

有了 some 函数,就可以通过传入如下数组检验一下结果

some([NaN,NaN,4], isNaN)
// true
some([3,4,4], isNaN)
// false

了解了 some 和 every 是如何工作的,下面来看看 sort 函数以及高阶函数如何在其中使用的。

3.3.3 sort 函数

sort 函数是 Array 原型的内置函数。假设我们需要给一个水果列表排序:

var fruit = ['cherries','apples','bananas'];

你可以简单地调用 sort 函数

fruit.sort();
// ['apples','bananas','cherries'];

它还可以接受一个 compare 函数,如果未提供的话,则会按照 unicode 编码顺序排序。我们可以通过自己提供的 compare 函数来排序任何 JavaScript 数据。sort 函数灵活的原因要归功于高阶函数的本质!

在编写 compare 函数之前,我们先看看它实际上应该实现什么

function compare(a, b){
    if(/* 根据某种排序标准 a < b */){
        return -1;
    }
    if(/* 根据某种排序标准 a > b */){
        return 1;    
    }
    return 0;
}

举个简单的例子,假设我们有一个人员列表

var people = {
    {firstname: "aaFirstName", lastname: "cclastName"},
    {firstname: "ccFirstName", lastname: "aalastName"},
    {firstname: "bbFirstName", lastname: "bblastName"},   
}

现在需要使用对象中的 firstname 键对人员进行排序,以如下形式传入 compare

people.sort((a,b) => {
    return (a.firstname < b.firstname) ? -1:(a.firstname > b.firstname) ? 1:0
    /*
        即
        if(a.firstname < b.firstname){
            return -1;
        }else if(a.firstname > b.firstname){
            return 1
        }else{
            return 0
        }
    */
})

上面的代码将返回如下数据

[
    { firstname: 'aaFirstName',lastName: 'cclastName'},
    { firstname: 'bbFirstName',lastName: 'bblastName'},
    { firstname: 'ccFirstName',lastName: 'aalastName'},    
]

根据 lastname 的排序如下

people.sort((a,b) => {
    return (a.lastname < b.lastname) ? -1:(a.lastname > b.lastname) ? 1:0
    /*
        即
        if(a.lastname < b.lastname){
            return -1;
        }else if(a.lastname > b.lastname){
            return 1
        }else{
            return 0
        }
    */
})

它将返回

[
    { firstname: 'ccFirstName',lastName: 'aalastName'},
    { firstname: 'bbFirstName',lastName: 'bblastName'},
    { firstname: 'aaFirstName',lastName: 'cclastName'},    
]

我们在看一下 compare 函数的逻辑

function compare(a, b){
    if(/* 根据某种排序标准 a < b */){
        return -1;
    }
    if(/* 根据某种排序标准 a > b */){
        return 1;    
    }
    return 0;
}

知道了 compare 函数的算法,我们能做得更好些吗?能把上面的逻辑抽象到一个函数中去吗?从上面的例子可以看到,我们用几乎重复的代码编写了两个函数去比较 firstname 和 lastname。我们将要设计的函数不会以函数Wie参数,但是会返回一个函数。

下面调用函数 sortBy,它允许用户基于传入的属性对对象数组排序,代码如下

// es6-functional.js
const sortBy = (property) => {
    // 返回一个函数
    return (a,b) => {
        var result = (a[property] < b[property]) ? -1:(a[property] > b[property]) ? 1:0
        return result;
        /*
            即
            if(a[property] < b[property]){
                return -1;
            }else if(a[property] > b[property]){
                return 1
            }else{
                return 0
            }
        */
    }
}

sortBy 函数接受一个名为 property 的参数并返回一个接受两个参数的新函数

返回函数清晰的描述了 compare 函数的逻辑

假设我们使用属性名 firstname 调用函数,函数体将替换 property 参数,如下所示

(a, b) => return (a['firstname'] < b['firstname']) ? -1:(a['firstname'] > b['firstname']) ? 1:0

我们通过手动编写了一个函数实现了想要的功能。sortBy 可以这样使用

// 根据 firstname 排序
people.sort(sortBy('firstname'))

// 返回
[
    { firstname: 'aaFirstName',lastName: 'cclastName'},
    { firstname: 'bbFirstName',lastName: 'bblastName'},
    { firstname: 'ccFirstName',lastName: 'aalastName'},    
]

// 根据 lastname 排序
people.sort(sortBy('lastname'))

// 返回
[
    { firstname: 'ccFirstName',lastName: 'aalastName'},
    { firstname: 'bbFirstName',lastName: 'bblastName'},
    { firstname: 'aaFirstName',lastName: 'cclastName'},    
]

和以前一样!

真是太棒了!sort 函数接受被 sortBy 函数返回的 compare 函数!有很多这样的高阶函数,我们再次抽象出了 compare 函数背后的逻辑,使得用户得以专注于真正的需求,毕竟,高阶函数就是抽象

3.4 小结

我们今天从 JavaScript 支持的数据结构开始,发现函数也是一种 JavaScript 数据结构。换句话说,函数能够被存储,传递和赋值,这种允许函数被传递给另一个函数被称为高阶函数。请记住,高阶函数是接受另一个函数作为参数或返回一个函数的函数。

在这一章中,我们看到了几个例子,它们展示出的高阶函数的概念可以帮助开发者将困难的部分抽象出来。还是建议大家动手写一写,这样才能加深自己的理解。

明天我们将学习闭包在高阶函数中的运行机制,see you tomorrow!

猜你喜欢

转载自blog.csdn.net/zhang6223284/article/details/82633079