本文ppt请笑纳
函数式编程
维基百科对函数式编程的定义:函数式编程(英语:functional programming)或称函数程序设计,是一种编程典范,它将计算机运算视为数学上的函数计算,并且避免使用程序状态以及易变对象。这里的函数指的是数学上的函数,既自变量(数据)的映射。
与命令式编程
对比,函数式编程
更加强调__程序执行的结果__而非__执行的过程__,倡导利用若干简单的执行单元(小函数)让计算结果不断渐进,逐层推导复杂的运算,而不是设计一个复杂的执行过程。
我的理解是,以纯函数( 避免使用程序状态以及异变对象)为单元,去抽象、拆分模块功能的编程思想。其实函数式编程我们平时不陌生,比如react, redux 都多多少少有一些相关。
所以我们得出几个函数式编程的要点:
- 避免使用程序状态以及异变对象,数据映射=> 纯函数
- 强调执行结果而非过程=> 声明式
纯函数
纯函数:输出结果只由输入决定,并且不产生副作用(effects)。
- 避免使用 this,window 等外部变量
- 没有产生副作用(Effects),既把Effects抽离出来
Effects有哪些呢?
比如 改动外部数据,发起ajax请求,改动dom等等。
写过dva的就知道,dva的model有一个effects,就是用来放这些副作用的代码。
我们把Effects分离出来之后,剩下的就是纯函数,纯函数可以很简单的进行单元测试,所以是不容易出bug的,因为往往有bug的都是不可预见的部分。
举个例子
1 2 3
| const arr=['bilibili', 'hello', 'jsonz']; arr.slice(1, 3); arr.splice(1, 2);
|
虽然执行之后,都能得到 Hello Jsonz
。 但是 splice 会改动到原来的数组,所以不算纯函数。
工作中其实很多工具类的function都是纯函数,redux的Reducer也是标准的纯函数。
redux reducers
1 2 3 4 5 6
| function set(state, { payload }) { return { ...state, ...payload, } }
|
纯函数的好处是,你不需要担心某个值在某处发送意想不到的改变导致出bug;纯函数里不产生副作用,所以也不会担心改动到其他地方,代码更健硕;可以更优雅的组合复用,方便移植到其他环境(web workers);方便的写单元测试。
声明式
在说声明式之前,要先普及一下与之对应的命令式(指令式)。
我们平时写的代码基本都是命令式,既我们通过一条一条的命令去执行一些操作,其中会涉及到很多细节的东西。
既关注执行过程,用各种控制语句if...else...
与for
循环等。
命令式编程:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
声明式编程:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
SQL语句是典型声明式,SQL存储过程和存储函数则是命令式编程。
react里面的两种编程方式1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const data= [{name: 'jsonz', age: 2}, { name: 'balala', age: 3}, { name: '小魔仙', age: 30}];
const dom = data.filter(item=> age<10) .map(item=> <span>{item.name} - {item.age}<span>); // 这里后面的span是闭合的</span>
// 命令式编程 const data= [{name: 'jsonz', age: 2}, { name: 'balala', age: 3}, { name: '小魔仙', age: 30}];
let newData = []; for (let i= 0; i< data.length; i++) { if (data[i].age < 10) { newData.push(data); } }
const dom2 = []; for (let i= 0; i< newData.length; i++) { dom2.push(`<span>${item.name}-${item.age}<span>`); }
|
我们可以看出声明式的代码更加简洁清晰优雅。
声明式代码复用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 27 28 29 30 31
| function double(arr) { const results = []; for (let i = 0; i< arr.length; i++) { results.push(arr[i] * 2); } return results; }
function addOne(arr) { const results = []; for (let i = 0; i< arr.length; i++) { results.push(arr[i] +1); } return results; }
function double(arr) { return arr.map(item=> item*2); }
function addOne(arr) { return arr.map(item=> item+1); }
const m= _.curry((fn, arr) => arr.map(item=> fn(item))); const double = m(item=> item*2); const addOne = m(item=> item+1);
|
这里我们可以看出一些函数式最简单的思路:
把循环和判断等控制语句尽可能换成筛选 filter
、映射 map
、化约 reduce
。
cookie获取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 27 28 29 30 31 32 33 34 35
|
var _cookie = document.cookie;
var _cookieArray = _cookie.split('; ');
var obj = {}; for (var i= 0; i< _cookieArray.length; i++) {
var cookieItem = _cookieArray[i];
var cookieItemArray = cookieItem.split('=');
var key = cookieItemArray[0]; var value = cookieItemArray[1]; obj[key] = value; }
document.cookie.split('; ') .map(item=> { const [key, value] = item.split('='); return {key, value}; }) .reduce((acc, cur)=> { const { key, value } = cur; acc[key] = value; return acc; }, {});
|
综合 纯函数 + 声明式
该部分源代码可以看这个仓库 ( ☉_☉)≡☞o────★°仓库地址
求完美数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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
|
{ let num = 6; function isPerfect() { return aliquotSum() === num; }
function isFactor(potential) { return num % potential === 0; }
function getFactors() { const arr = [];
arr.push(1); arr.push(num);
for (let i= 2; i< num; i++) { if (isFactor(i)) arr.push(i); }
return arr; }
function aliquotSum() { let sum = 0; const factors = getFactors(num); for (let i= 0; i< factors.length; i++) { sum += factors[i]; } sum -= num; return sum; }
const result = isPerfect(); console.log(result); }
{ function isPerfect(num) { return aliquotSum(num) === num; }
function isFactor(num, potential) { return num % potential === 0; }
function getFactors(num) { return Array.from(Array(num), (item, i)=> i) .filter(v=> isFactor(num, v)); }
function aliquotSum(num) { let sum = 0; const factors = getFactors(num); for (let i= 0; i< factors.length; i++) { sum += factors[i]; } return sum; }
const result = isPerfect(6); console.log(result); }
{ const aliquotSum = num=> Array.from(Array(num), (item, i)=> i) .filter(item=> num % item === 0) .reduce((cur, next)=> cur += next); function isPerfect(num) { return aliquotSum(num) === num; } const result = isPerfect(6); console.log(result); }
|
上面都是比较小的工具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 24
| { let count = 0; const button = document.querySelector('#button'); button.addEventListener('click', ()=> { count += 1; console.log(`clicked!${count} times`); }); }
{ const { fromEvent, operators: { scan, } } = rxjs; const button = document.querySelector('#button');
fromEvent(button, 'click') .pipe( 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 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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| requirejs.config({ paths: { ramda: 'https://cdnjs.cloudflare.com/ajax/libs/ramda/0.13.0/ramda.min', jquery: 'https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min' } });
require([ 'ramda', 'jquery' ], function (_, $) {
const Impure = { getJSON: _.curry((callback, url)=> $.getJSON(url, callback)),
setHtml: _.curry((sel, html)=> $(sel).html(html)), };
const img = function(url) { return $('<img />', { src: url }); };
const mediaUrl = _.compose(_.prop('m'), _.prop('media'));
const srcs = _.compose(_.map(mediaUrl), _.prop('items'));
const images = _.compose(_.map(img), srcs);
const url = term=> `https://api.flickr.com/services/feeds/photos_public.gne ?tags=${term}&format=json&jsoncallback=?`;
const renderImages = _.compose(Impure.setHtml('body'), images);
const app = _.compose(Impure.getJSON(renderImages), url);
app('cat');
});
|
优化
1 2 3 4 5 6 7 8 9 10 11
| const mediaUrl = _.compose(_.prop('m'), _.prop('media')); const srcs = _.compose(_.map(mediaUrl), _.prop('items'));
const images = _.compose(_.map(img), srcs);
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 纯函数式编程语言
什么是函数式编程思维
函数式编程术语解析