JS 函数式编程思维简述(三):柯里化

JavaScript010

JS 函数式编程思维简述(三):柯里化,第1张

       在探讨柯里化之前,我们首先聊一聊很容易跟其混淆的另一个概念—— 偏函数(Partial Application) 。在维基百科中,对 Partial Application 的定义是这样的:

其含义是:在计算机科学中,局部应用(或偏函数应用)是指将 多个参数 固定在一个函数中,从而 产生另一个函数 的过程。

举个例子,假设我们是一个加工厂,用于生产梯形的零件,生产过程中我们要根据 订单来源方 给的一系列参数计算面积:

突然有一天,我们发现了一个问题:我们的大部分订单零件,都是高度为 28 的规格,此时面积函数调用经常是这个样子的:

此时,我们便可以 以第一个函数为模板 ,来创建 存储了固定值的新的计算函数

当然,这个示例中并没有以明显的 偏函数 的方式去呈现,我们可以让返回结果变成一个新的函数,因此我们可以加以改造:

也可以将其简化为:

这里,我们就可以将 trapezoidAreaByHeight15() 、 trapezoidAreaByHeight28() 和 trapezoidAreaByHeight33() 视为 trapezoidArea() 的偏函数。

        偏函数 往往不能改变一个函数的行为,通常是根据一个已有函数而生成一个新的函数,这个新的函数具有已有函数的相同功能,区别在于在新的函数中有一些参数 已被固定 不会变更。偏函数的设计通常:

        柯里化(Currying) 是以美国数理逻辑学家哈斯凯尔·科里(Haskell Curry)的名字命名的函数应用方式。 与偏函数很像的地方是:都可以缓存参数,都会返回一个新的函数,以提高程序中函数的适用性。 而不同点在于, 柯里化(Currying) 通常用于分解原函数式,将参数数量为 n 的一个函数,分解为参数数量为 1 的 n 个函数,并且支持连续调用。例如:

可见, 柯里化(Currying) 用于将多元任务分解成单一任务,每一个独立的任务都 缓存了上一次函数生成时传递的入参 ,并且让新生成的函数更简单、专注。上述演变也可以写作:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。这里例举一个在 JavaScript 中用于做强制类型判断的示例:

使用这一的方式构建的函数 checkType() 具备了高通用性,但适用性则略差。我们发现 每次的调用过程,使用者都需要编写参数 typeStr 表示的类型字符串 ,增加了函数的应用复杂度。此时作为设计者,就可以对该函数加以改造,使其生成多个具备高适用性的独立函数:

        柯里化(Currying) 分解了函数设计过程,将运行的步骤拆分为每一个单一参数的 lambda 演算。我们可以通过递归的方式,来构造出一个可进行无限调用,并返回相同的累加函数的 柯里化函数

调用方式如:

以这样的方式,我们构建的参数是一个简单对象 nexter ,该对象至少包含一个 value 属性,用于描述本次累加的值。如果希望获取累加结果,则为 nexter 对象赋予函数属性 success 即可。结果会以实参的形式,传递给 success 函数用于传递通知。

        Promise 对象无论是构造函数还是后续的链式调用中,都能看到 柯里化 设计的影子:接收单一参数,返回一个 Promise :

调用方式为:

开始切入正题之前,有必要告知大家一下,这篇文章可能有一些深度,初学者可能理解会有些吃力。我会尽量把复杂问题简单化,争取让每个阅读的童鞋们都能看得懂。希望你对element-ui,vue-router,KeepAlive组件有一点了解。现在,我们开始吧。

熟悉element-ui的童鞋们都知道,ElMenuItem和ElSubMenu都需要一个index属性,该属性必须是唯一的。现在,我们想把路由配置直接应用于ElMenu,该如何确保index的唯一性呢?我们需要有一个生成唯一index的函数。如下是genKey函数的定义,是不是很简单?

现在,我们创建addRouteMetaKey函数,该函数对路由树进行递归遍历,为每一个路由配置的meta属性动态添加key字段。这个函数很简单,属于最基础的递归使用例子,我就不做太多解释了。

提示 :数组的forEach方法不是纯函数,因为它有副作用,所以forEach方法不能称之为函数式编程工具。

通过addRouteMetaKey函数,我们可以把路由的meta.key作为index的值了。

现在,我们想实现另一个功能,就是 基于标签页的路由组件缓存控制 。使用过Vuejs KeepAlive组件的童鞋们,应该多多少少都遇到一些坑吧?在我们的项目中,有一个顶部标签页导航,每个tab项对应一个url,以每个url对应路由的title作为tab项标题,当切换tab的时候加载缓存中的url对应的路由组件,关闭tab项,下次打开该路由url,重新挂载对应的路由组件,而不是从缓存中加载。

当路由层级不确定的时候,KeepAlive会变的难以手动控制。所以,我 剑走偏锋,尝试了一种简单的解决方案 ,实践证明: 这是非常有效的 。我的方案就是把无限层级的路由树转化为一维数组。通过为meta添加必要的字段,进行页面结构个性化定制。

我们需要把每层路由配置的path转化为全路径,所以需要一个函数:getRouteFullPath,该函数定义如下:

getRouteFullPath函数中使用到的joinPath函数用于将多个路径字符串拼接为1个完整的路径,定义如下:

现在,我们把路由树转化为一维数组。我们定义toFlatRoutes函数,该函数使用了数组的reduce方法对路由树进行聚合递归,将路由配置中的path属性的值替换为全路径,还顺便给路由配置添加了name属性,返回一个新的一维路由配置数组。 这是函数式编程和递归结合的一个例子。

以上,我们解决了KeepAlive的缓存控制问题;现在,我们又有了一个新的用户体验上的需求,就是我们想根据url对应的路由,自动展开该路由meta.key所属的侧边菜单;我们通过查阅element-ui文档得知,ElMenu有一个open方法,接收index作为参数,展开index对应的菜单。

现在的问题是,我们的路由对应的index是ElMenuItem的,而路由树已经被我们转化为了一维数组,通过路由的matched字段是得不到我们想要的菜单index的。所以, 我们需要遍历原始路由树

如下代码,我们定义getMenuKey函数,该函数接收的参数为route对象。如果路由存在,我们进行查找。首先进行简单查找,如果找到一个菜单menu,则返回该菜单的meta.key;如果简单查找无果,则对路由树进行递归查找; 这是函数式编程和递归结合的另一个例子。

现在,我们大功告成了;以上就是本节的知识点,童鞋们理解了吗?只要我们熟悉递归的使用,其实操作树很简单。如果大家还有不懂的,可以评论区问我。感谢阅读!

写一个函数(即方法:function),然后去调用这个方法、比如写个C的helloworld然后调用printf就是函数式(过程化)编程,

补充:JavaScript一种直译式脚本语言,是一种动态类型、弱类型、基于原型的语言,内置支持类型。它的解释器被称为JavaScript引擎,为浏览器的一部分,广泛用于客户端的脚本语言,最早是在HTML(标准通用标记语言下的一个应用)网页上使用,用来给HTML网页增加动态功能。

在1995年时,由Netscape公司的Brendan Eich,在网景导航者浏览器上首次设计实现而成。因为Netscape与Sun合作,Netscape管理层希望它外观看起来像Java,因此取名为JavaScript。但实际上它的语法风格与Self及Scheme较为接近。