在前面几篇介绍了函数式比较重要的一些概念和如何用函数组合去解决相对复杂的逻辑。是时候开始介绍如何控制副作用了。

数据类型

我们来看看上一篇最后例子:

1
2
3
4
5
6
7
const split = curry((tag, xs) => xs.split(tag))
const reverse = xs => xs.reverse()
const join = curry((tag, xs) => xs.join(tag))

const reverseWords = compose(join(''), reverse, split(''))

reverseWords('Hello,world!');

这里其实reverseWords还是很难阅读,你不知道他入参是啥,返回值又是啥。你如果不去看一下代码,一开始在使用他的时候,你应该是比较害怕的。 “我是不是少传了一个参数?是不是传错了参数?返回值真的一直都是一个字符串吗?”。这也是类型系统的重要性了,在不断了解函数式后,你会发现,函数式编程和类型是密切相关的。如果在这里reverseWords的类型明确给出,就相当于文档了。

但是,JavaScript是动态类型语言,我们不会去明确的指定类型。不过我们可以通过注释的方式加上类型:

1
2
// reverseWords: string => string
const reverseWords = compose(join(''), reverse, split(''))

上面就相当于指定了reverseWords是一个接收字符串,并返回字符串的函数。

JS 本身不支持静态类型检测,但是社区有很多JS的超集是支持类型检测的,比如Flow还有TypeScript。当然类型检测不光是上面所说的自文档的好处,它还能在预编译阶段提前发现错误,能约束行为等。

当然我的后续文章还是以JS为语言,但是会在注释里面加上类型。

我们都知道单一职责原则,其实面向对象的SOLID中的S(SRP, Single responsibility principle)。在函数式当中每一个函数就是一个单元,同样应该只做一件事。但是现实世界总是复杂的,当把现实世界映射到编程时,单一的函数就没有太大的意义。这个时候就需要函数组合和柯里化了。

链式调用

如果用过jQuery的都晓得啥是链式调用,比如$('.post').eq(1).attr('data-test', 'test').javascript原生的一些字符串和数组的方法也能写出链式调用的风格:

1
'Hello, world!'.split('').reverse().join('') // "!dlrow ,olleH"

首先链式调用是基于对象的,上面的一个一个方法split, reverse, join如果脱离的前面的对象”Hello, world!”是玩不起来的。

最近在看Typescript,顺便看了一些函数式编程,然后半个国庆假期就没有了。做个笔记,分几个部分写吧。

最开始接触函数式编程的时候,第一个接触的概念就是高阶函数,和柯里化。咋一看,这不就是长期用来讲作用域的demo吗?我在日常也有用啊,有啥吗?

其实呢,设计模式或则编程范式往往不在于技巧,而在于思想。函数式编程就是一种编程的范式,并不在于技巧多么叼,而在于它的思想。其次才是由设计思想才衍生出来的技巧,技巧往往而言是服务于思想的。所以我觉得最开始学习函数式编程最好先了解一些相关概念和思想会比较好。

函数是一等公民(first class)

如果理解直接看为一等公民的好处好处。

其实说函数式一等公民的意思就是说函数和其他“公民”具有相同的属性。就像任何一种数据类型,它能够被存储在数组中,能够作为函数的参数,能够赋值给变量:

1
2
3
const hello = (name) => (`Hello ${name}!`)
const sayHello = hello; // 作为变量
const helloArray = [hello, sayHello]

上面的代码没有什么意义,只是表达函数在JavaScript中是一等公民,和一个值一样。

函数是一等公民的好处

拿一个callback的例子来讲,比如你用fetch发个请求时:

1
2
3
fetch('getPostLink')
.then(res => renderPosts(res))
.catch(err => handleError(error))

上面其实可以直接传递一个函数作为回调,加一层包裹其实没有必要:

1
fetch('getPostLink').then(renderPosts).catch(handleError)

多一层函数的包裹并没有任何意义,完全是多余的代码。再看一个例子:

1
2
3
4
5
const postController = {
find(postId) { return Db.find(postId) },
delete(postId) { return Db.delete(postId) },
...
}

上面的代码其实就是聚合一些功能作为一个对象,但是多加了一层的函数,也是没有必要的,在阅读的时候到会增加复杂度,其实postController.find === Db.find,所以完全没有再去包裹一层函数:

1
2
3
4
5
const postController = {
find: Db.find,
delete: Db.delete,
...
}

上面的代码是不是更表意,然而如果js的函数不能像值一样传递,上面的简写都是不可能的。上面的代码其实还有一个好处,你不用去纠结如何命名在两层函数之间的参数了。这种风格代码是符合Pointfree的,我们后面要介绍。另外,函数式编程是操作函数的,所以函数是一等公民也是函数式的基石,基本上如果js不支持这一项,函数式根本玩不转。

在接触过React项目后,大多数人都应该已经了解过或则用过了HOC(High-Order-Components)和FaCC(Functions as Child Components),因为这两个模式在大多数react的开源库里都存在。比如react-router里面的withRouter 就是典型的高阶组件,接受一个组件返回另外一个经过增强后的组件。而react-motion中的Motion就是典型的FaCC的应用。

HOC和FaCC两者做的事也是非常相似的,都是类似设计模式里面的装饰者模式。都是在原有的实例或则单元上进行功能的增强。

当然不只是一些开源库中会使用,在平常的代码编写中,也有很多地方是适用于使用HOC和FaCC去封装一些逻辑。比如数据埋点,新特性的toggle,获取转换数据等。对于增强代码可读性和逻辑复用来说,非常有用的。

console.time & console.timeEnd

为了比较具体代码快的运行速度我们需要一些度量工具。console.timeconsole.timeEnd是浏览器原生就支持的属性。具体用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function generate100Array() {
var arr = new Array(100);
for(var i = 0; i < 100; i++){
arr[i]='';
}
return arr
}
var a = generate100Array()
var b = generate100Array()
console.time('for')
for (var i = 0, len = a.length; i < len; i++) {
a[i] = i
}
console.timeEnd('for')
console.time('forEach')
b.forEach(function (el, index) {
b[index] = index
})
console.timeEnd('forEach')

你可以复制粘贴上面代码到chrome 终端运行一下,你会发现forforEach快,在我的chrome62.0 大概是快3-4倍。列出的理论,我都会提供相应代码,大家可以自己去试试。我只会在chrome下实验,并给出比较结果。