函数式编程
维基百科对函数式编程的定义:函数式编程(英语:functional programming)或称函数程序设计,是一种编程典范,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。这里的函数指的是数学上的函数,既自变量(数据)的映射。
与命令式编程
对比,函数式编程
更加强调程序执行的结果而非执行的过程,倡导利用若干简单的执行单元(小函数)让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
我的理解是,以纯函数( 避免使用程序状态以及异变对象)为单元,去抽象、拆分模块功能的编程思想。其实函数式编程我们平时不陌生,比如react, redux 都多多少少有一些相关。
所以我们得出几个函数式编程的要点:
- 避免使用程序状态以及异变对象,数据映射=> 纯函数
- 强调执行结果而非过程=> 声明式
纯函数
纯函数:输出结果只由输入决定,并且不产生副作用(effects)。
- 避免使用 this,window 等外部变量
- 没有产生副作用(Effects),既把Effects抽离出来
Effects有哪些呢?
比如 改动外部数据,发起ajax请求,改动dom等等。
写过dva的就知道,dva的model有一个effects,就是用来放这些副作用的代码。
我们把Effects分离出来之后,剩下的就是纯函数,纯函数可以很简单的进行单元测试,所以是不容易出bug的,因为往往有bug的都是不可预见的部分。
举个例子1
2
3const arr=['bilibili', 'hello', 'jsonz'];
arr.slice(1, 3); // 没副作用
arr.splice(1, 2); // 有副作用,修改到原来的arr数组 可能会造成意想不到的bug
虽然执行之后,都能得到 Hello Jsonz
。 但是 splice 会改动到原来的数组,所以不算纯函数。
工作中其实很多工具类的function都是纯函数,redux的Reducer也是标准的纯函数。
redux reducers1
2
3
4
5
6function set(state, { payload }) {
return {
...state,
...payload,
}
}
纯函数的好处是,你不需要担心某个值在某处发送意想不到的改变导致出bug;纯函数里不产生副作用,所以也不会担心改动到其他地方,代码更健硕;可以更优雅的组合复用,方便移植到其他环境(web workers);方便的写单元测试。
声明式
在说声明式之前,要先普及一下与之对应的命令式(指令式)。
我们平时写的代码基本都是命令式,既我们通过一条一条的命令去执行一些操作,其中会涉及到很多细节的东西。
既关注执行过程,用各种控制语句if...else...
与for
循环等。
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
SQL语句是典型声明式,SQL存储过程和存储函数则是命令式编程。
1 | // 函数式编程 |
我们可以看出声明式的代码更加简洁清晰优雅。
1 | // 双倍 |
这里我们可以看出一些函数式最简单的思路:
把循环和判断等控制语句尽可能换成筛选 filter
、映射 map
、化约 reduce
。
1 |
|
综合 纯函数 + 声明式
该部分源代码可以看这个仓库 ( ☉_☉)≡☞o────★°仓库地址
1 | // 完全数(Perfect number),又称完美数或完备数,是一些特殊的自然数。 它所有的真因子(即除了自身以外的约数)的和(即因子函数),恰好等于它本身 |
上面都是比较小的工具demo,基本上不包含Effects,但是实际上项目充斥一堆业务逻辑(Effects),所以一般会借助一些工具/库来实现,比如 ramda.js
和rx.js
。
rxJs
官网的描述是 Reactive Extensions Library for JavaScript简单来说RxJS就是辅助我们写出函数式响应式代码的一种工具。
我们现在来实现一个功能,页面上有一个按钮,点击之后输出计数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 普通版本
{
let count = 0;
const button = document.querySelector('#button');
button.addEventListener('click', ()=> {
count += 1; // 用到外部变量
console.log(`clicked!${count} times`);
});
}
// RxJs版本
{
const { fromEvent, operators: { scan, } } = rxjs;
const button = document.querySelector('#button');
fromEvent(button, 'click')
.pipe(
// scan 随着时间的推移进行归并。
scan((count)=> count+ 1, 0)
)
.subscribe(count=> console.log(`clicked! ${count} times`));
}
节流和累加点击坐标1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26{
let count = 0;
const button = document.querySelector('#button');
const rate = 1000;
let lastClick = Date.now() - rate;
button.addEventListener('click', (e)=> {
if (Date.now() - lastClick >= rate) {
count += e.clientX;
console.log(`clicked!${count} times`);
lastClick = Date.now();
}
});
}
{
const { fromEvent, operators: { scan, throttleTime, map } } = rxjs;
const button = document.querySelector('#button');
fromEvent(button, 'click')
.pipe(
throttleTime(1000),
map(e=> e.clientX),
scan((count, clientX)=> count+ clientX, 0)
)
.subscribe(count=> console.log(`clicked! ${count} times`));
}
接下来我们用Ramda.js
来实现一个页面,获取某个关键字图片,并展示出来。Ramda
主要帮忙实现一些 curry
, compose
等功能
1 | requirejs.config({ |
优化1
2
3
4
5
6
7
8
9
10
11// old
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));
const srcs = _.compose(_.map(mediaUrl), _.prop('items'));
const images = _.compose(_.map(img), srcs);
// new
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));
const images = _.compose(_.map(img), _.map(mediaUrl), _.prop('items'));
再优化1
const images = _.compose(_.map(_.compose(img, mediaUrl)), _.prop('items'));
函数式和面向对象对比
面向对象编程思想,封装
继承
多态
。
把状态的改变封装起来,让代码更加清晰,通过类与继承去处理关联关系。但是会充满公用变量,this
,bind
等。
面向对象目前更加广泛,而且更容易被接受,写起来更容易,但可能会有更多冗余的代码,捆绑了很多状态,典型的问题是 你只需要一个香蕉,但是却得到一个拿着香蕉的大猩猩以及整个丛林。
函数式编程思想,尽量减少不确定因素,来让代码更加清晰健硕。
每个函数都不要去修改原有的数据,而是通过产生新的数据来做为运算结果,这也意味着函数式比较耗资源,所以在计算机石器时代并不流行。
简单来说就是很多小的纯函数,一个小函数只做一些事情,然后用这些小函数来组织成更大的函数,函数的参数与返回值也大部分都是函数。
最后能得到一个超级牛逼的函数,我们只要把数据给他,就可以了。
函数式简洁,清晰可复用性高,出错几率更小。但是一开始思维很难从面向对象或命令式转换过来,往往也需要借助一些工具库来编写。
结语
这里只提了函数式编程的一些思想,但是真正函数式开发还包含很多其他要学的。比如我们用到的柯里化curry
还有代码组合compose
等,光RxJs
提供的api就有179个之多…
再者不知道是不是对函数式的理解不够透彻,有试过某个需求用函数式的思想去写,但是有点好像硬掰的样子,写的有点不伦不类….所以现在只是吸收一些函数式的思想(比如纯函数减少不确定性),但是平时工作不会很刻意全部都用函数式去写。只是当成一个可以扩展的知识面,后面如果函数式真的在前端流行起来,再上手也不迟。
顺路感慨JS的灵活性,既有ES6面向对象的特性class
也有ES5的 filter
、map
、reduce
,以及ES.Next的装饰器(Decorator)等函数式编程的特性。
最后再给出参考的以及学习过程看到的一些不错的书籍和文章:
JS 函数式编程指南
深入浅出Rxjs
函数式编程思维
命令式、声明式、面向对象、函数式、控制反转之华山论剑(上)
RxJs 响应式代码库
Ramda 函数式工具库
Haskell 纯函数式编程语言
什么是函数式编程思维
函数式编程术语解析