高阶函数与高阶组件

1,808 阅读11分钟
原文链接: blog.5udou.cn

前言

初次听闻这个概念是在去年的时候,那会是为了解决Eslint中关于React的bind报错的问题,有个同事推荐说使用高阶函数就可以了,但是那会一知半解的,不知高阶函数为何物?高阶组件也是没有听过。如今时隔半年有余,接触的信息多了,就觉得有必要好好总结一下这个所谓的”高阶“概念,以及在我们日常的编程中有个什么意义。

今天的话题是函数式编程的一部分,函数式编程被吹捧的一个优点就是可以写出更短更精简重复度少的代码,但从我最近的学习来说函数式编程思想显得更加重要。

1、高阶函数概念

根据wiki的解释:

在数学上和计算科学上,一个高阶函数应该具备下面至少一个特点:

将一个或者多个函数作为形参
返回一个函数作为其结果

这次wiki的解释很清晰易懂(难得呀),那为什么叫高阶呢?因为这种函数可以被调用很多次,你想想看,我在高阶函数中如果返回一个函数,那么你又可以调用这个函数,如果你返回的函数中又返回一个函数,那么如此下去就可以调用N多次。类似的在高等数学中有高阶导数(指的是两阶以上的导数),求导之后返回的结果可以再次被求导。

在Js这门语言中最常用的高阶函数想必是map和reduce。如下面的例子:

var array = [1, 2, 3, 4, 5]
var newArray = array.map(item => item*2)
var result = newArray.reduce((sum, item) => {
    return sum += item
})
console.log(newArray, result)

map和reduce函数都使用函数作为参数,所以满足我们的特征,还有一个也很常见的函数就是sort。更多高阶函数大家自行搜索。

谈到高阶函数的时候我们还不得不提到另外两个概念:柯里化组合

1.1、函数柯里化

还是引用wiki的解释:

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

如此官方的术语想必是很难真正理解柯里化(wiki通常是这样)。其实柯里化通常也称部分求值(partial application),其含义是给函数分步传递参数,每次传递参数中部分应用参数,并返回一个更具体的函数接受剩下的参数,这中间可嵌套多层这样的接受部分参数函数,直至返回最后结果。

因此柯里化的过程是逐步传参,逐步缩小函数的适用范围,逐步求解的过程。

用一个简单的例子来解释这个:

function originalAdd(a, b) {
    return a + b
}

function add(a) {
    return function (b) {
        return a + b
    }
}

var tmp = add(5)
var result = tmp(5)
console.log(result)

我们按照上面的解释再结合这个例子来说明函数是如何柯里化的:

  1. 首先我们将函数的参数拆分,分为a和b(可以有更多的参数)
  2. 然后我们在add函数中传入部分的应用参数(也就是啊,此时等于5)
  3. 返回一个更加具体的函数来处理剩余的参数(也就是参数b,在剩余的过程利用闭包来实现暂存参数a)
  4. 最后返回结果

当然这个例子只是为了说明柯里化的概念,实际的柯里化会比这个稍微复杂一点,大家如果能够先有一个大致概念,那么接下去的解释可以更加容易接受。

1.1.1、函数柯里化的实现

函数柯里化在lodash已经是作为一个API可以使用,官网给出的demo如下:

var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = _.curry(abc);

curried(1)(2)(3);

curried(1, 2)(3);

利用_curry将add这个函数柯里化,柯里化后的add函数可以链式调用,也可以随意参数,这就是柯里化函数的魅力所在。

本来想找到lodash源码看一下,但找到curry.js后找不到下面这个文件:

import createWrap from './.internal/createWrap.js'

难道我打开的是假的lodash源码?

那么我们自己实现一个简易的类lodash的curry函数:

function curry(fn) {
    var _args = [];
    return function () {
        [].push.apply(_args, [].slice.call(arguments));
        if (_args.length === fn.length) {
            const args = _args
            _args = []
            return fn.apply(this, args);
        }

        return arguments.callee;
    }
}
var abc = function(a, b, c) {
  return [a, b, c];
};

var curried = curry(abc)

console.log(curried(1)(2)(3))

console.log(curried(1, 2)(3))

该实现只有当传入的参数个数总数等于需要柯里化函数的参数个数,才会实际调用。

1.1.2、函数柯里化的作用
  1. 延迟计算:上面的例子可以看出来,只有等到具备了所有的参数后真正进行计算
  2. 参数复用:比如1.1节的第一个例子中,
    var tmp = add(5)
    var result = tmp(5)
    
    得到的tmp函数就不再需要传递参数5,因为这个参数是固定的,而一直改变的参数是后面再调用tmp(5)中的参数5,你可以改成任意一个数字。 也就是说你可以使用柯里化来固定那些已知恒定不变的参数,然后返回一个函数去处理那些未知的参数(易变的参数),等到你有合适的值传递的时候再调用,这也恰恰是柯里化的思想。这样就可以达到参数复用的目的。
  3. 动态创建函数。这个作用援引浅析 JavaScript 中的 函数 currying 柯里化这篇文章(感谢作者),文中提到的这个例子很是生动:
 var addEvent = function(el, type, fn, capture) {
     if (window.addEventListener) {
         el.addEventListener(type, function(e) {
             fn.call(el, e);
         }, capture);
     } else if (window.attachEvent) {
         el.attachEvent("on" + type, function(e) {
             fn.call(el, e);
         });
     } 
 };

 应该改为:

 var addEvent = (function(){
    if (window.addEventListener) {
        return function(el, sType, fn, capture) {
            el.addEventListener(sType, function(e) {
                fn.call(el, e);
            }, (capture));
        };
    } else if (window.attachEvent) {
        return function(el, sType, fn, capture) {
            el.attachEvent("on" + sType, function(e) {
                fn.call(el, e);
            });
        };
    }
})();

修改后的实现只需要调用一次判断,之后再使用时不需要再判断的,现实的代码中有很多都可以改成这种写法,会优化很多的代码!

这个作用的大致含义原文也解释了:

这可以是在部分计算出结果后,在此基础上动态生成新的函数处理后面的业务,这样省略了重复计算。或者可以通过将要传入调用函数的参数子集,部分应用到函数中,从而动态创造出一个新函数,这个新函数保存了重复传入的参数(以后不必每次都传)。

另外还有一个更加常见的例子是:Function.prototype.bind,bind 方法将第一个参数设置为函数执行的上下文,其他参数依次传递给调用方法(函数的主体本身不执行,可以看成是延迟执行),并动态创建返回一个新的函数,等到必要的时候再去调用,这符合柯里化的特点。

1.1.2、函数柯里化总结

柯里化是一个非常有用的函数式编程技巧。它允许你去生成一个小巧简易一致性的配置函数库,我们可以快速地使用并且可读性还很好。添加柯里化到你的代码实践中将会鼓励你在你的代码中使用部分计算的功能,避免大量的代码重复,并且也许会帮助你养成命名和处理函数参数的好习惯。

1.2、函数组合

函数组合的概念也是函数式编程的一部分,顾名思义,组合多个函数得到一个新的函数,类似于高等数学中的表达式:z = g(f(x)。比如改写高阶函数中的第一个例子为:

var array = [1, 2, 3, 4, 5]
var composeFun = function(array) {
    return array.map(item => item*2)
                .reduce((sum, item) => {
                    return sum += item
                })
}

console.log(composeFun(array))

我们使用map和reduce组合出一个新的函数有点类似于函数的封装,不过你是否发现这样写的一个缺点了?

如果有多个操作组合起来,看着会写出很多行.的操作,这种链式操作的写法,可读性不高。

那么如果使用更加compose的写法呢?

lodash已经为函数组合的写法实现了一个API,以前是叫做compose的,现在改成了flowRight,与flow(在函数编程中实际应该是对应pipe概念)形成对比,主要是一个从右边执行(类似于webpack中loader的执行过程),另外一个是左边执行(比较符合人类思维),那么使用这个API,例子改成:

var array = [1, 2, 3, 4, 5]
function reduce (array) {
    return array.reduce((sum, item) => {
    return sum += item
  })
}
function map (array) {
    return array.map(item => item*2)
}
var composeFun = _.flowRight(reduce, map)

console.log(composeFun(array))

这就是组合函数的使用。可以查看lodash的源码,它其实也就是循环执行函数,并将上一个函数的结果传递给下一个函数而已,但我们需要这种简易明了的写法来让我们的代码更加具备更高可读性和更好的维护性。

1.3、结论

好了,整个高阶函数到此讲完,作为函数式编程的一部分,我们需要好好消化然后掌握,运用到平时的代码编写中,让函数式编程思想深入到自己的脑海中,让我们写的JS代码更加简明易懂。后续我会不断学习函数式编程,希望有更多的总结。

2、高阶组件

讲完高阶函数,那么我们视线转到高阶组件,第一次接触高阶组件是从React的官方文档获知,然后当时的这句话是在没有掌握高阶函数的时候去阅读的,有点一知半解,现在呢?想必应该是能够举一反三了吧。

a higher-order component is a function that takes a component and returns a new component.

根据官方文档,高阶组件在第三方React库很常见,比如Redux的connect和Relay的createContainer

在没有HOC的时候,官方是推荐使用mixin来实现组件的继承扩展的。Mixins 的做法很像传统的命令式编程,即要扩展的组件决定需要哪些扩展(Mixins),以及了解所有扩展(Mixins)的细节,从而避免状态污染。当 Mixins 多了之后,被扩展组件需要维护的状态和掌握的”知识”越来越多,因此也就越来越难维护,因为责任都被交给了”最后一棒”(Last Responsible Moment)。

而高阶组件的思路则是函数式的,每一个扩展(Enhance)就是一个函数,接受要扩展的组件作为参数。如果要进行 3 个扩展,那么则可以级联,看起来就是:

const newComponent = Enhance3(Enhance2(Enhance1(MyComponent)));

高阶组件的方式则使得每一个 Enhance 以及被扩展的组件都只关心自己手里那点事。Enhance 不知道别人会怎么用它,被扩展的组件也不关心别人会怎么扩展它。这种分而治之的理念让代码更加容易维护。React为了更好让开发组更加标准的使用高阶组件,在其官网上列举了一些约定,详细可以参考官方文档。

其中一个约定倒是有必要拿出来说一下:

HOC既不会修改输入的组件,也不会使用使用继承性去拷贝输入组件的行为,相反HOC通过包裹它在一个容器组件来组合原始组件,HOC是一个纯函数没有任何副作用。也就是说HOC可以往被扩展的组件注入自己的东西,但是不允许去改动被扩展组件原有的一切东西。

Tips:纯函数并且没有任何副作用(side-effect)

纯函数指的是如果一个函数执行完后不会对全局变量以及入参产生任何修改,此时认为该函数是纯函数,并且没有任何副作用。比如:

function pureFunc (input) {
    return 100
}

但是如果这样的函数就不是纯函数:

function notPureFunction (input) {
    input = {}
    window.xxx = 'i overwrite'
}

更多细节可以参考React官方文档,写得很是详细。高阶组件的应用可以参考这篇文章深入理解React的高阶组件

高阶组件后续应该可以再单独出一篇文章来介绍的,这里限于篇幅就不再细说,先给大家一个基本的印象吧。

参考

  1. en.wikipedia.org/wiki/Higher…
  2. en.wikipedia.org/wiki/Curryi…
  3. lodashjs.com/docs/#_flow…
  4. www.cnblogs.com/zztt/p/4142…
  5. jcouyang.gitbooks.io/functional-…
  6. en.wikipedia.org/wiki/Functi…
  7. www.jianshu.com/p/4780d82e8…
  8. medium.com/@franleplan…