Doraemonls

函数式JavaScript:每天都能用的函数组合

原文链接: hackernoon.com

图片来源:PIRO4D

函数组合 现在是函数式编程里我最喜欢的一部分。我希望能在本文里给你一些实用的例子,好让你能理解什么是函数组合,这样你也可以每天都用!这篇文章里,我们会学习如何组织你的js文件,这样你就能写出像下面这样简洁清晰的函数式代码了:

import { listGroupPanel } from './lib/html'
import { setInnerHtml }   from './lib/dom'

const content = document.getElementById('content')
const main = e => compose(setInnerHtml(e), listGroupPanel)

const list = [
  'Cras justo odio',
  'Dapibus ac facilisis in',
  'Morbi leo risus',
  'Porta ac consectetur ac',
  'Vestibulum at eros'
]

main(content)(list)

最终代码的输出是:

<div class="panel panel-default">
  <div class="panel-body">
    <ul class="list-group">
      <li class="list-group-item">Cras justo odio</li>
      <li class="list-group-item">Dapibus ac facilisis in</li>
      <li class="list-group-item">Morbi leo risus</li>
      <li class="list-group-item">Porta ac consectetur ac</li>
      <li class="list-group-item">Vestibulum at eros</li>
    </ul>
  </div>
</div>

如果你想改动完善一下,直接去codepen。好,从代码开始学吧!

基本知识点

要想跑步先会走路,先从一些你必要的枯燥点的内容开始。

函数组合是一个数学概念,可以将两个或多个功能组合成一个新功能。

当谷歌函数组合时,你可能会偶然发现下面这个例子。不过如果你还没看到过,我保证,你肯定会的搜到的。

const add = x => y => x + y
const multiply = x => y => x * y
const add2Multiply3 = compose(multiply(3), add(2))

I myself am guilty of using this example and what I failed to realize is that the student is not yet able to see how this can be practically applied in their codebase today. Instead they are comparing that example with something like this: 用这个例子我其实有点内疚,因为我才意识到同学们还不能把这个方法直接在日常工作中使用。相反的,大家会把例子和下面的代码相比较:

const value = (x + 2) * 3

这样的比较很难让人们选择使用函数式方法。

一个老师如果不能用现实世界里好的例子让学生理解原因,那他就是失败的。

希望我能阐述清函数组合的力量。

回到基础知识

函数组合的关键在于要有能够组合的函数。一个组合的函数应当有一个输入值和一个输出值。

通过柯里化,你可以把任何一个函数变成一个可组合的函数。我会在另一篇文章里详谈柯里化函数。不过你不需要了解柯里化也能理解本文内容。

你也许在写html的代码,那就从这里开始把。让我们先创建一个tag。(我准备从字符串开始讲,不过你在React里也可以这样做。)

const tag = t => contents => `<${t}>${contents}</${t}>`
tag('b')('this is bold!')

> <b>this is bold!</b>

我还要增加一个处理带属性标签的函数,就像这样:<div class="title">...</div>。所以就又有一个这样的函数:

const encodeAttribute = (x = '') =>
  x.replace(/"/g, '"')

const toAttributeString = (x = {}) =>
  Object.keys(x)
    .map(attr => `${encodeAttribute(attr)}="${encodeAttribute(x[attr])}"`) 
    .join(' ')

const tagAttributes = x => c =>
  `<${x.tag}${x.attr?' ':''}${toAttributeString(x.attr)}>${c}</${x.tag}>` 

代码稍微重构一下,这样就能组合四个函数在一起了:

const encodeAttribute = (x = '') =>
  x.replace(/"/g, '"')

const toAttributeString = (x = {}) =>
  Object.keys(x)
    .map(attr => `${encodeAttribute(attr)}="${encodeAttribute(x[attr])}"`) 
    .join(' ')

const tagAttributes = x => (c = '') =>
  `<${x.tag}${x.attr?' ':''}${toAttributeString(x.attr)}>${c}</${x.tag}>`

const tag = x =>
  typeof x === 'string'
    ? tagAttributes({ tag: x })
    : tagAttributes(x)

现在,我们就可以通过一个string或者object调用tag函数了。

const bold = tag('b')

bold('this is bold!')
// <b>this is bold!</b>

tag('b')('this is bold!')
// <b>this is bold!</b>

tag({ tag: 'div', attr: { 'class': 'title' }})('this is a title!') 
// <div class="title">this is a title!</div>

来点干货

讲完枯燥的部分,现在有必要加点料了。

下面我们就用这个tag函数来作出点新东西。我们可以从简单熟悉的地方开始,bootstrap’s list group.

首先,让我们给每个tag创建一个函数,然后再给每一个listGroupItems 来支持多个listGroupItems

看一下list-group的结构,就能发现这里有一个最外层的元素,它包含了很多子节点。既然结构都如此,那我们就可以创建一个listGroup(listGroupItems([‘Cras justo’, ‘Dapibus ac’]))的方法,来渲染这个结构。

我应该只需要调用 listGroup([‘Cras justo’, ‘Dapibus ac’])方法就行了。这个函数应该了解我要做什么。

因此,我需要两个方法,listGrouplistGroupTag。这样我就可以通过 listGroup 创建一个列表,然后通过listGroupTag来封装这个列表,listGroupTag(listGroupItems([]))

函数组合

有些读者可能直接跳过前面的段落,直接看这一章,不过你可能还是会失望。组合函数其实是个很简单的过程。在你创建可组合的函数后,它们就能顺利的组合在一起了。

以下面代码为例。你只要认出类似模式,那么这类函数就可以方便得组合起来。

const listGroup = items =>
  listGroupTag(listGroupItems(items))

组合函数后,结果就很像最初的代码样例,左边是listGroupTag ,接下来就是 listGroupItems,右侧是 items

const listGroup = items => compose(listGroupTag, listGroupItems)(items)

让我们把这两种方法放在一起,对比一下差异和相似之处。

        listGroupTag (listGroupItems (items))
compose(listGroupTag, listGroupItems)(items)

函数组合后,它们是从右向左阅读的,就像普通的函数一样。

因为compose返回的函数是一个 list,我们的listGroup 也是以list 作为输入,因此我们就可以把listGroup简化成一个组合函数并去掉list参数。

const listGroup = compose(listGroupTag, listGroupItems)

现在你的思路可能没跟上,我能理解。在我们编写的所有这些代码中,函数组合只帮助我们简化了一行代码。

随着代码库的增长,函数组合让你可以创造更多的新组合,它的强大之处就在这里。

让我们加一个bootstrap的panel面板。

const panelTag = tag({ tag: 'div', attr: { class: 'panel panel-default' }});
const panelBody = tag({ tag: 'div', attr: { class: 'panel-body' }});
const basicPanel = compose(panelTag, panelBody);

假设我们要在 panel添加一个list-group,我们只需这样做:

const listGroupPanel =  compose(basicPanel, listGroup);

上面的函数和下面这个是等效的。你可以组合任意数量的函数:

const listGroupPanel =  compose(basicPanel, listGroupTag, listGroupItems);

组织你的代码

组织代码也非常重要。因为你要把函数分离到多个文件中去。

我通常创建一个叫做functional.js的文件,我会把 compose和相关的一些函数放在这里。

上面提到的所有的代码我会放在html.js去。

我还为Dom操作专门创建了一个dom.js,(下面codepen有样例)。

把代码分成不同的库文件使我们能在不同项目中复用这些函数。

现在我们开始写main.js里的主要部分了,这里的代码就比较少了。

代码样例

Codepen的例子里不能把文件分成多个,所以我在注释里标明了,这样你就可以参考怎么合理组织你的文件了。

我把最终的app放在codepen上,这样你就可以添砖加瓦了。 https://codepen.io/joelnet/pen/QdVpwB

关于数学部分

本来我计划再多写一点,不过你并不需要知道重力方程来理解对象抽象。

### 组合和管道

还有一个值得一提的知识点,和compose 常常同时出现的还有一个函数 pipepipe也是组合函数,不过是相反的顺序写的。在某些情况下,从左向右写代码更容易理解。

`// pseudo code`
const login = pipe(
  validateInput,
  getCustomer,
  getAuthToken
  loginResponse);

这就是各种各样的函数了,我强烈推荐把这些函数放在functional.js 的库文件里,这样你就可以在所有的项目里引用了。

总结

函数组合要求用一种可组合的方式编写你的函数,这意味着函数必须要有一个输入和输出。有多个参数的函数需要柯里化才能组合。

函数组合并不简单,但其乐无穷。

有了函数组合,你可以在代码复用的道路走的更远。

提高代码复用性永远都是我们的目标之一。

我的下一篇文章是关于异步函数, 我原以为可以把这两部分合起来一起写。不过那样内容就太长了。所以就此停笔。

不想错过第二部分的话,赶紧订阅我!

最后一件小事,我收到Medium和Twitter(@joelnet)的关注通知时,都非常开心。或者你觉得我在瞎扯,也可以在评论区留言。

干杯!