collins

V1

2022/04/14阅读:29主题:自定义主题1

前端基石:高阶函数之柯里化、组合函数、惰性思想

前言

在理解柯里化、组合函数和惰性思想之前,我们想来理解一下高阶函数。高阶函数英文叫 Higher-order function。那么什么是高阶函数?

先看一段代码:

function add(x, y, f) {
    return f(x) + f(y);
}

当我们调用add(-5, 6, Math.abs)时,参数xyf分别接收-56和函数Math.abs,根据函数定义,我们可以推导计算过程为:

x = -5;
y = 6;
f = Math.abs;
f(x) + f(y) ==> Math.abs(-5) + Math.abs(6) ==> 11;
return 11;

JavaScript 的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。

柯里化

柯里化是什么?柯里化是什么可能 100 个人有 100 种理解。

  • ✅ 百度百科:在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
  • ✅ 菜鸟教程:柯里化(Currying)指的是将原来接受两个参数的函数变成新的接受一个参数的函数的过程。新的函数返回一个以原有第二个参数为参数的函数。
  • ✅ 知乎 1:柯里化(Currying)指将多元函数转化为多个一元函数连续定义。
  • ✅ 知乎 2:柯里化(Currying)又称部分求值(Partial Evaluation),简单来说就是只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

但是就我个人理解,柯里化(Currying)是一种编程思想,函数执行产生一个闭包,把一些信息预先存储起来,目的是供下级上下文使用。这样预先存储和处理的思想,就叫做柯里化的编程思想。

柯里化也是面试题中的常客,有一个比较经典的题目,那就是求和。

add(1)(2)(3)(4)(5)(6); // => 21
add(1, 2)(3, 4)(5, 6); // => 21
add(1, 2, 3, 4, 5, 6); // => 21

这是一个比较经典的面试题,包括我自己在面试的时候也遇到了这个面试题。乍一看这就是一个柯里化,但它其实比柯里化要复杂一些,单纯的柯里化实现不了这种调用。

原因在于普通的柯里化是不能做到一直调用来无限增加参数的,柯里化是有一个结束的,但是这个题目中明显是没有结束的。

每一次调用的参数个数是不知道的,调用的次数也是不知道的。所以每一次返回的肯定不是一个数值,只能是一个函数。那这里还有一个问题,如果都是返回的函数,当需要结束时如何处理?这里有一个细节知识点。

知识点:对象(包括数组,对象,函数等)参与原始运算如算术或逻辑运算时,会无参调用其 toString 或者 valueOf 方法得到一个原始值,然后用这个原始值参与运算,这点上应该是借鉴自 Java,但规则好像比 Java 要复杂,具体的我也没有太深究过,毕竟 JavaScrip t 里面我们很少利用这个特性(所以很多人其实不知道)。能够持续调用,必然返回函数,能够当成数值,那只能是因为它实现了 toString 或者 valueOf 方法。

基于以上,实现思路如下(当然不是唯一):

var curring = () => {
  var result = [];
  var add = (...args) => {
    result = result.concat(args);
    return add;
  };

  add.valueOf = add.toString = () => {
     return result.reduce((pre, cur) => pre + cur, 0);
  }
  return add;
};

var add = curring();
console.log(+add(1)(2)(3)(4)(5)(6));

add = curring();
console.log(+add(1, 2)(3, 4)(5, 6));

add = curring();
console.log(+add(1, 2, 3, 4, 5, 6));

组合函数

这里简单先勾勒一下 函数式编程和命令式编程。

函数式编程:把具体执行的步骤放到到一个函数中,后期需要用的时候,直接调用函数就可以了,不用在关心函数里面是怎么实现的。函数式编程侧重的是结果。函数式编程的优缺点都比较明显:

  • 优点:低耦合、高内聚,便捷开发,便于维护。
  • 缺点:不够灵活,不能对步骤做特殊处理。

命令式编程:和函数式编程不同的是,命令式编程关注的是步骤,需要我们自己去实现每一步。

  • 优点:灵活,每一步都能去处理。
  • 缺点:代码冗余,不够高效。 基于函数式编程和命令式编程的优缺点,提倡在日常的使用中,使用函数式编程。

在函数式编程中,有一个很重要的概念就是函数组合,实际上就是把处理的函数数据像管道一样连接起来,然后让数据穿过管道连接起来,得到最终的结果。例如:

var a = (x) => x + 1;
var b = (x) => x * 2;
var c = (x) => x - 1;
var res = c(b(a(a(1))));
console.log(res); // => 5

但是这种写法,可读性比较差,所以我们可以构建一个组合函数,函数接受任意函数为参数,每个函数只能接受一个参数,最后组合函数也返回一个函数。例如:

var resFn = compose(c, b, a, a);
resFn(1); // => c(b(a(a(1)))) => 5

组合函数,其实大致思想就是将 c(b(a(a(1)))) 这种写法简写为 compose(c, b, a, a)(x) 。但是注意这里如果一个函数都没有传入,那就是传入的是什么就返回什么,并且函数的执行顺序是和传入的顺序相反的。这两点需要注意。

实现如下:

var compose = (...funcs) => {
  // funcs(数组):记录的是所有的函数
  // 这里其实也是利用了柯里化的思想,函数执行,生成一个闭包,预先把一些信息存储,供下级上下文使用
  return (x) => {
    var len = funcs.length;
    // 如果没有函数执行,直接返回结果
    if (len === 0) return x;
    if (len === 1) funcs(x);
    return funcs.reduceRight((res, func) => {
      return func(res);
    }, x);
  };
};
var resFn = compose(c, b, a, a);
resFn(1);

组合函数的思想,在很多框架中也被使用,例如:redux,实现效果来说是其实和上面的代码等价。

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

惰性思想

惰性函数,字面意思懒惰的函数,但是“大智若愚”,惰性函数并不是真的懒惰,反而它是聪明的函数,为什么说它是聪明的了,因为它不会去重复地去做某一样东西,而形成冗余。而这也恰好是它的作用,好处!!!

惰性函数优点:就是能避免多次重复的步骤判断,冗余等,只需一次判定,即可直接去使用,不用做无用的重复步骤。

惰性函数的应用场景:常用于函数库的编写,单例模式之中。在固定的应用环境不会发生改变,频繁要使用同一判断逻辑的。

在日常的项目中,其实我们很多地方都可以运用到惰性思想。例如要封装一个获取元素属性的方法,因为低版本的 ie 浏览器不支持 getComputedStyle 方法,做了一个容错处理:

function getCss(element, attr) {
    if ('getComputedStyle' in window) {
        return window.getComputedStyle(element)[attr];
    }
    return element.currentStyle[attr];
}

但是每次进这个方法都要做一下判断,为了提高代码的可维护性,我们可以存一个变量,然后每次进去判断变量就好了

var flag = 'getComputedStyle' in window
function getCss(element, attr) {
    if (flag) {
        return window.getComputedStyle(element)[attr];
    }
    return element.currentStyle[attr];
}

但是每次执行 getCss 函数的时候,都需要判断执行,那有没有一种方式可以优化了,这时候惰性思想就可以用上了。

function getCss(element, attr) {
    if ('getComputedStyle' in window) {
        getCss = function (element, attr) {
            return window.getComputedStyle(element)[attr];
        };
    } else {
        getCss = function (element, attr) {
            return element.currentStyle[attr];
        };
    }
    // 为了第一次也能拿到值
    return getCss(element, attr);
}

getCss(document.body, 'margin');
getCss(document.body, 'padding');
getCss(document.body, 'width');

第一次执行,如果有 getComputedStyle 这个方法,getCss 就被赋值成

function (element, attr) {
    return window.getComputedStyle(element)[attr];
};

而后的每一次就会执行上面这个函数。

总结

  • 柯里化思想,利用闭包,把一些信息预先存储起来,目的是供下级上下文使用。
  • 组合函数,把处理的函数数据像管道一样连接起来,然后让数据穿过管道连接起来,得到最终的结果。
  • 惰性思想,不去重复地去做某一样东西,而形成冗余。

三种思想都是常见的高阶函数思想,也是除回调函数之外,用的比较多的三种高阶函数。

参考

分类:

前端

标签:

JavaScript

作者介绍

collins
V1