函数式编程之闭包与高阶函数

4. 闭包与高阶函数

前两天去面试了,所以没时间看书,准备把剩下的几家面完就提前结束秋招了,面试太累了。话不多说,我们开始今天的函数式编程的学习。

前两天我们了解了高阶函数如何抽象通用的问题,我们创建了一个 sortBy 高阶函数并展示了一个有效的相关用例。

在继续函数式编程之前,闭包是我们需要理解的概念。这也是我们今天要讨论的主要问题,我们将详细闭包并应用它编写高阶函数。让我们开始吧。

4.1 理解闭包

虽然我觉得既然你看这篇文章,说明对于基础已经理解了,不过还是简要介绍一下闭包吧,一直知道的可以直接跳过这部分

简言之,闭包就是一个内部函数,先看一个最基本的例子吧

var a = 1;
function inner(){
    a = a + 1;
    return a;
}
inner() // 2

这是一段最基本的代码了,应该都没什么疑问吧。平常应该都会写,其实这已经是一个闭包了。

让我们来分析一下这段代码是怎么运行的,调用 inner 的时候,发现函数内部并没有 a 这个变量,于是到包含这个函数的作用域即全局作用域去寻找,找到了,所以这里使用的 a 其实就是外部 a。

为什么这就是一个闭包了呢???

我们来看上面的定义,闭包就是一个内部函数,所以这里就是指的 inner 函数。那么它的外部函数呢?这里当然指的就是全局执行环境啦,全局执行环境就是 js 代码整个的执行环境,模拟一下

(function global(){
    var a = 1;
    function inner(){
        a = a + 1;
        return a;
    }
    inner() // 2
})()

怎么样,这样就能看出是一个闭包了吧。当然,正常的闭包应该是下面这种方式

let global = 'outer'
function outer(){
    let global = 'inner';
    let a = 5;
    function inner(){
        console.log(global)
        console.log(a++);
    }
    return inner;
}
var b = outer();
// 执行完这一步 b = inner
b(); // inner 5;
b(); // inner 6;

这就是一个正常的闭包了,我们来分析一下

首先,outer() 代表运行这个函数,并返回结果 inner,这里就是将这个结果赋值给 b。

然后执行 b,其实执行的就是 inner,我们看看执行 inner 会发生什么,首先打印 global,但是函数内部没有啊?那怎么办,其实跟上面一样,刚刚是什么找到 a 的,现在就怎么找 global,只不过多加了一层函数而已,所以这里就在 outer 函数内部找到了 global,那么 outer 函数外部的 global 呢。这里就和原型链一样了,返回的是找到的第一个值。那么对于 a++ 呢,第一次返回 5 也没有问题,第二次呢?一般情况下,如果函数里面的变量没有继续被引用的话就会被销毁,但是我们考虑这么一个情况,还是第一个函数

var a = 1;
function inner(){
    a = a + 1;
    return a;
}
inner() // 2
console.log(a); // 2

这里的 a 会被销毁吗,不会,为什么,因为 js 主线程一直在运行,所以这个 a 一直存在。对应到上面的 b,正常情况下,outer 函数执行完以后,内部的 global,a,inner 应该都会被销毁的,但是这里为什么没有被销毁呢。我们对比上面的例子。b 引用了外部的 a,outer 内部的 global 变量,所以导致这个变量保存了下来。

用一句简单的话来说,内部函数调用时保存了外部参数的值并能在后续中维持这些参数。当然这么说不严谨,但是这么理解应该没什么问题。

闭包这个函数有如下几个可访问的作用域

  • 在它自身声明之内的变量
  • 全局变量
  • 包含闭包函数的函数里面的变量,这里也就是 a 和 global

如果觉得还是不太清楚的话,可以留言,我后面会详细的讲一下。

4.2 真实的高阶函数(续)

有了对闭包的理解,我们可以实现一些真实有用的高阶函数

4.2.1 tap 函数

由于我们要在函数式编程中处理很多函数,因此需要一种调试方式。

下面实际一个名为 tap 的简单函数。tap 函数接受一个 value 并返回一个包含 value 的闭包函数,该函数将被执行

const tap = (value) => {
    (fn) => (
        (typeof(fn) === 'function' && fn(value)),
        console.log(value)
    )
}

这里用到了逗号表达式,逗号表达式就是计算逗号前面的值,并返回后面的值,例如

var a = 1;
// 先计算 a++,然后返回 a + 2
var b = (a++,a+2);
console.log(b) // 4

将 tap 函数改成 ES5 的语法即

var tap = function(value){
    return function(fn){
        return (
            // 这句话的意思其实就是如果 fn 是函数的话,计算 fn(value),然后打印 value,否则直接打印 value
            (typeof(fn) === 'function' && fn(value)),
            console.log(value)
        )
    }
}
// 使用 tap 函数
tap('fun')((it) => console.log('value is',it))
// value is fun
// fun

// 参数不是函数,直接打印 value
tap('fun')()
// fun

在这里分享一个很巧妙的运用逗号表达式的例子

'helloworld'.split('').reduce((p,k) => ((p[k]++||(p[k]=1)),p),{})

效果如下

1536926915206

按顺序统计出了各个字符的出现次数,但是具体为什么是这样的话有些不太明白,所以希望有能看懂的大佬讲解一下

4.2.2 unary 函数

接下来我们看另一个函数

有这么一个例子相信大家都遇到过

['1','2','3'].map(parseInt);
// [1,NaN,NaN]

为什么会这样呢,因为 parseInt 接受两个参数,第一个要转换的字符串,第二个是按几进制转换(如果为 0 或者没有默认以 10 进制转换),而这里,map 的第二个参数 index 会默认作为 parse 的第二个参数,所以就产生了上面的结果,那么我们该怎么只传一个参数进去呢?这就是我们这节要讲的 unary 函数

unary 函数接受一个给定的多参数函数,并把它转换成一个只接受一个参数的函数

const unary = (fn) => {
   return fn.length === 1 ? fn : (arg)=>fn(arg)
}

我们检查传入的 fn 是否有一个长度为 1 的参数列表,如果有,就什么也不做。如果没有,就返回一个函数,它只接受一个参数 arg,并用该参数调用 fn

让我们试一下

['1','2','3'].map(unary(parseInt));
// [1, 2, 3]

这样我们就得到了正确的结果。

4.2.3 once 函数

在很多情况下,我们只需要运行一次给定的函数,比如只想设置一次第三方库,或初始化一次支付设置等。这一节我们将编写一个 once 的高阶函数,它允许开发者只运行一次给定的函数。

const once = (fn) => {
    let done = false;
    return function(){
        return done ? undefined : ((done = true),fn.apply(this,arguments));
    }
}

once 函数只接受一个参数 fn 并调用它的 apply 方法,我们声明了一个 done 变量,当第一次调用的时候,使用了逗号表达式将其转换为 true,然后执行 fn 函数。第二次调用时,因为 done 为 true 了,会返回 undefined,阻止了后续的执行,让我们来试一下

var payment = function(){
    console.log('已付款');
}
var doPayment = once(payment);
doPayment(); // 已付款
doPayment(); // undefined

4.2.4 memoized 函数

假设有一个阶乘函数 factorial

var factorial = (n) => {
    if(n === 0){
        return 1;
    }
    return n * factorial(n - 1);
}

我们输入 2,得到 3,输入 3,得到 6,没什么特别的,但是如果我们输入一次的话,就会把之前的所有阶乘都重新计算一遍,那么有没有方法可以将之前的结果缓存下来呢?这就是 memoized 函数要做的事情,它使函数能记住其计算结果,来看一下

const memoized = (fn) => {
    const cache = [];
    return (arg) => cache[arg] || (cache[arg] = fn(arg));
}

在上面的函数中,我们定义了一个名为 cache 的局部变量,它在返回函数的闭包上下文中,返回函数将接受一个参数并检查它是否存在,如果存在返回对应的值,否则将新的输入作为 key,重新计算结果并更新 cache

现在我们再来看看加了缓存的阶乘函数

let fastFactorial = memoized((n) => {
    if(n === 0){
        return 1;
    }
    return n * fastFactorial(n - 1);
});
// 调用
fastFactorial(5); // 120
fastFactorial(7); // 5040

它和之前的函数运行方式相同,但是比之前快的多,这就是高阶函数之美——闭包和纯函数的实战!

4.3 小结

今天我们介绍了一下闭包,理解了闭包如何记住函数上下文。并根据这层理解编写了一些常见的高阶函数,明天我们将继续构建高阶函数,但是我们会把关注点转向数组!那明天见啦。

猜你喜欢

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