1、引言javascript
函数式编程的历史已经很悠久了,可是最近几年却频繁的出如今大众的视野,不少不支持函数式编程的语言也在积极加入闭包,匿名函数等很是典型的函数式编程特性。大量的前端框架也标榜本身使用了函数式编程的特性,好像一旦跟函数式编程沾边,就很高大上同样,并且还有一些专门针对函数式编程的框架和库,好比:RxJS、cycleJS、ramdaJS、lodashJS、underscoreJS等。函数式编程变得愈来愈流行,掌握这种编程范式对书写高质量和易于维护的代码都大有好处,因此咱们有必要掌握它。html
2、什么是函数式编程前端
维基百科定义:java
函数式编程(英语:functional programming),又称泛函编程,是一种编程范式,它将电脑运算视为数学上的函数计算,而且避免使用程序状态以及易变对象。程序员
3、纯函数(函数式编程的基石,无反作用的函数)es6
在初中数学里,函数f的定义是:对于输入x产生一个惟一输出y=f(x)。这即是纯函数。它符合两个条件:数据库
1.此函数在相同的输入值时,老是产生相同的输出。函数的输出和当前运行环境的上下文状态无关。编程
2.此函数运行过程不影响运行环境,也就是无反作用(如触发事件、发起http请求、打印/log等)。json
简单来讲,也就是当一个函数的输出不受外部环境影响,同时也不影响外部环境时,该函数就是纯函数,也就是它只关注逻辑运算和数学运算,同一个输入总获得同一个输出。api
javascript内置函数有很多纯函数,也有很多非纯函数。
纯函数:
Array.prototype.slice
Array.prototype.map
String.prototype.toUpperCase
非纯函数:
Math.random
Date.now
Array.ptototype.splice
这里咱们以slice和splice方法举例:
var xs = [1,2,3,4,5]; // 纯的 xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] xs.slice(0,3); //=> [1,2,3] // 不纯的 xs.splice(0,3); //=> [1,2,3] xs.splice(0,3); //=> [4,5] xs.splice(0,3); //=> []
咱们看到调用数组的slice方法每次返回的结果彻底相同,同时xs不会被改变,而调用splice方法每次返回值都不同,同时xs变得面目全非。
这就是咱们强调使用纯函数的缘由,由于纯函数相对于非纯函数来讲,在可缓存性、可移植性、可测试性以及并行计算方面都有着巨大的优点。
这里咱们以可缓存性举例:
var squareNumber = memoize(function(x){ return x*x; }); squareNumber(4); //=> 16 squareNumber(4); // 从缓存中读取输入值为 4 的结果 //=> 16
那咱们如何把一个非纯函数变纯呢?好比下面这个函数:
var minimum = 21; var checkAge = function(age) { return age >= minimum; };
这个函数的返回值依赖于可变变量minimum的值,它依赖于系统状态。在大型系统中,这种对于外部状态的依赖是形成系统复杂性大大提升的主要缘由。
var checkAge = function(age) { var minimum = 21; return age >= minimum; };
经过改造,咱们把checkAge变成了一个纯函数,它不依赖于系统状态,可是minimum是经过硬编码的方式定义的,这限制了函数的扩展性,咱们能够在后面的柯里化中看到如何优雅的使用函数式解决这个问题。因此把一个函数变纯的基本手段是不要依赖系统状态。
4、函数柯里化
curry 的概念很简单:将一个低阶函数转换为高阶函数的过程就叫柯里化。
用一个形象的比喻就是:
好比对于加法操做:var add = (x, y) => x + y,咱们能够这样柯里化:
//es5写法 var add = function(x) { return function(y) { return x + y; }; }; //es6写法 var add = x => (y => x + y); //试试看 var increment = add(1); var addTen = add(10); increment(2); // 3 addTen(2); // 12
对于加法这种极其简单的函数来讲,柯里化并无什么用。
还记得上面的checkAge函数吗?咱们能够这样柯里化它:
var checkage = min => (age => age > min); var checkage18 = checkage(18); checkage18(20); // =>true
这代表函数柯里化是一种“预加载”函数的能力,经过传递一到两个参数调用函数,就能获得一个记住了这些参数的新函数。从某种意义上来说,这是一种对参数的缓存,是一种很是高效的编写函数的方法:
var curry = require('lodash').curry; //柯里化两个纯函数 var match = curry((what, str) => str.match(what)); var filter = curry((f, ary) => ary.filter(f)); //判断字符串里有没有空格 var hasSpaces = match(/\s+/g); hasSpaces("hello world"); // [ ' ' ] hasSpaces("spaceless"); // null var findSpaces = filter(hasSpaces); findSpaces(["tori_spelling", "tori amos"]); // ["tori amos"]
5、函数组合
假设咱们须要对一个字符串作一些列操做,以下,为了方便举例,咱们只对一个字符串作两种操做,咱们定义了一个新函数shout,先调用toUpperCase,而后把返回值传给exclaim函数,这样作有什么很差呢?
不优雅,若是作得事情一多,嵌套的函数会很是深,并且代码是由内往外执行,不直观,咱们但愿代码从右往左执行,这个时候咱们就得使用组合。
var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = function(x){ return exclaim(toUpperCase(x)); }; shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
使用组合,咱们能够这样定义咱们的shout函数:
//定义compose var compose = (...args) => x => args.reduceRight((value, item) => item(value), x); var toUpperCase = function(x) { return x.toUpperCase(); }; var exclaim = function(x) { return x + '!'; }; var shout = compose(exclaim, toUpperCase); shout("send in the clowns"); //=> "SEND IN THE CLOWNS!"
代码从右往左执行,很是清晰明了,一目了然。
咱们定义的compose像N面胶同样,能够将任意多个纯函数结合到一块儿。
这种灵活的组合可让咱们像拼积木同样来组合函数式的代码:
var head = function(x) { return x[0]; }; var reverse = reduce(function(acc, x){ return [x].concat(acc); }, []); var last = compose(head, reverse); last(['jumpkick', 'roundhouse', 'uppercut']); //=> 'uppercut'
6、声明式和命令式代码
命令式代码:命令“机器”如何去作事情(how),这样无论你想要的是什么(what),它都会按照你的命令实现。
声明式代码:告诉“机器”你想要的是什么(what),让机器想出如何去作(how)。
与命令式不一样,声明式意味着咱们要写表达式,而不是一步一步的指示。
以 SQL 为例,它就没有“先作这个,再作那个”的命令,有的只是一个指明咱们想要从数据库取什么数据的表达式。至于如何取数据则是由它本身决定的。之后数据库升级也好,SQL 引擎优化也好,根本不须要更改查询语句。这是由于,有多种方式解析一个表达式并获得相同的结果。
这里为了方便理解,咱们来看一个例子:
// 命令式 var makes = []; for (var i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 声明式 var makes = cars.map(function(car){ return car.make; });
命令式的循环要求你必须先实例化一个数组,并且执行完这个实例化语句以后,解释器才继续执行后面的代码。而后再直接迭代 cars 列表,手动增长计数器,就像你开了一辆零部件所有暴露在外的汽车同样。这不是优雅的程序员应该作的。
声明式的写法是一个表达式,如何进行计数器迭代,返回的数组如何收集,这些细节都隐藏了起来。它指明的是作什么,而不是怎么作。除了更加清晰和简洁以外,map 函数还能够进一步独立优化,甚至用解释器内置的速度极快的 map 函数,这么一来咱们主要的业务代码就无须改动了。
函数式编程的一个明显的好处就是这种声明式的代码,对于无反作用的纯函数,咱们彻底能够不考虑函数内部是如何实现的,专一于编写业务代码。优化代码时,目光只须要集中在这些稳定坚固的函数内部便可。
相反,不纯的不函数式的代码会产生反作用或者依赖外部系统环境,使用它们的时候老是要考虑这些不干净的反作用。在复杂的系统中,这对于程序员的心智来讲是极大的负担。
7、Point Free
pointfree 模式指的是,永远没必要说出你的数据。它的意思是说,函数无须说起将要操做的数据是什么样的。一等公民的函数、柯里化(curry)以及组合协做起来很是有助于实现这种模式。
// 非 pointfree,由于提到了数据:word var snakeCase = function (word) { return word.toLowerCase().replace(/\s+/ig, '_'); }; // pointfree var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
这种风格可以帮助咱们减小没必要要的命名,让代码保持简洁和通用。固然,为了在一些函数中写出Point Free的风格,在代码的其它地方必然是不那么Point Free的,这个地方须要本身取舍。
8、示例应用
拥有了以上的知识,咱们是时候该写一个示例应用了。
这里咱们使用了 ramda ,没有用 lodash 或者其余类库。ramda 提供了 compose、curry 等不少函数。
咱们的应用将作四件事:
1.根据特定搜索关键字构造 url
2.向 flickr 发送 api 请求
3.把返回的 json 转为 html 图片
4.把图片放到屏幕上
上面提到了两个不纯的动做,即从 flickr 的 api 获取数据和在屏幕上放置图片这两件事。咱们先来定义这两个动做,这样就能隔离它们了。这里咱们只是简单包装了一下jQuery的getJSON函数,把它变为一个 curry 函数,还有就是把参数位置也调换了下,咱们把它们放在 Impure 命名空间下以用来隔离,这样咱们就知道它们都是危险函数。
运用函数柯里化和函数组合的技巧,咱们就能够建立一个函数式的实际应用了:
1 var _ = R; 2 var Impure = { 3 getJSON: _.curry(function(callback, url) { 4 $.getJSON(url, callback); 5 }), 6 7 setHtml: _.curry(function(sel, html) { 8 $(sel).html(html); 9 }) 10 }; 11 12 var img = function (url) { 13 return $('<img />', { src: url }); 14 }; 15 16 //////////////////////////////////////////// 17 18 var url = function (t) { 19 return '你的接口地址'; 20 }; 21 22 var mediaUrl = _.compose(_.prop('m'), _.prop('media')); 23 24 var srcs = _.compose(_.map(mediaUrl), _.prop('items')); 25 26 var images = _.compose(_.map(img), srcs); 27 28 var renderImages = _.compose(Impure.setHtml("body"), images); 29 30 var app = _.compose(Impure.getJSON(renderImages), url); 31 32 app("cats");
看看,多么美妙的声明式规范啊,只说作什么,不说怎么作。如今咱们能够把每一行代码都视做一个等式,变量名所表明的属性就是等式的含义。
9、总结
咱们已经见识到如何在一个小而不失真实的应用中运用新技能了,可是异常处理以及代码分支呢?如何让整个应用都是函数式的,而不只仅是把破坏性的函数放到命名空间下?如何让应用更安全更富有表现力?
我会在下一篇文章中(JavaScript函数式编程(二))介绍函数式编程的更加高阶一些的知识,例如Functor、Monad、Applicative等概念。