常用的编程思想有一下几类:
1、面向过程编程,最初级的,想到哪写到哪;
2、面向对象编程,以事物为中心的编程思想,把共有的属性和方法封装到一个类里;
3、面向切面编程,统计一个函数执行的时间;
4、函数式编程,提纯无关于业务的纯函数,函数嵌套让函数更强大。(react中大量使用)
一、函数式编程思维及核心概念
1、函数作为“一等公民”,可以赋值给其他变量,也可以作为参数,传入另一个函数,或者作为别的函数的返回值。
2、map和reduce是最常见的函数式编程方法
var arr = [11, 22, 33];
arr.map(function(value, index, array){
console.log(value, index, array);
})
//11 0 [11, 22, 33]
//22 1 [11, 22, 33]
//33 2 [11, 22, 33]
这里面.map本身就是一个函数
3、对于函数式编程,里面的纯函数,对于同样的输入,一定会有同样的输出,永远不依赖于外部状态。
var xs = [1,2,3,4,5];
var result1 = xs.slice(0, 3);
console.log('xs:', xs);
console.log('result1:', result1);
// xs: [1, 2, 3, 4, 5]
//result1: [1, 2, 3]
var result2 = xs.splice(0, 3);
console.log('xs', xs);
console.log('result2', result2);
// xs: [4, 5]
//result2: [1, 2, 3]
可以发现Array.slice是一个纯函数,没有副作用,对于固定的输入总有固定的输出。
纯函数可以记忆(同样的输入总有同样的输出),不和外界有任何关系,抽象代码方便单元测试
4、函数的柯里化函数,函数接受一堆参数,返回一个新函数,继续接收参数能够处理逻辑。
柯里化之前:
var addFoo = (x, y) => x+y;
addFoo(1, 2); //3
柯里化之后:
var addFoo = x => ( y => x+y);
addFoo(1)(2); //3
此处只是对柯里化概念的解释,其实一般简单的处理是没必要柯里化的。
柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种高效的编写函数的方法。
5、函数组合
纯函数以及如何把它柯里化写出洋葱代码h(g(f(x))),为了解决柯里化函数所最后生成的洋葱样的代码,需要用到“函数组合”
用函数柯里化改写下面函数,让多个函数像拼积木一样:
const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(firtst, reverse);
last([1, 2, 3, 4, 5]); //5
其实这里compose函数就是first函数和reverse函数组合在一起拼接成的一个组合函数,先求数组的逆序函数,再得出数组的第一个值。从最里层一层一层往外层剥开。
函数组合相当于把一页一页的洋葱贴在一起。
compose(f, compose(g,h))
compose(compose(f, g), h)
compose(f, g, h)
6、Point Free
把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。
const f = str => str.toUpperCase().split(' ');
这个函数中,使用了str作为中间变量,但这个中间变量除了让代码变得长一点以外,毫无意义。
var toUpperCase = word.toUpperCase();
var split = x =>(str => str.split(x));
var compose = (f, g) => (x => f(g(x)));
var f = compose(split(' '), toUpperCase);
f("abc def"); //ABCDEF
这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。
7、惰性函数
惰性函数,只在第一次执行,第一次执行后再调用得到的结果都是一样的。对于浏览器来说,节省了时间和资源。
一个简单的例子如下:
function test(a) {
if (a == 1) {
test = function () {
return "ok"
}
return "ok";
}else{
test = function (argument) {
return "no";
}
return "no";
}
}
test(1) // "ok"
test(10) //"ok"
当第一次执行test(1)后,函数的返回值固定返回“ok”了,所以不管传的参数是多少,都是返回“ok”。
最典型的一个应用就是ajax中:
function createXHR(){
var xhr=null;
if(typeof XMLHttpRequest!='undefined'){
xhr=new XMLHttpRequest();
createXHR=function(){
return XMLHttpRequest(); //直接返回一个懒函数,这样不必在往下走
}
}else{
try{
xhr=new ActiveXObject("Msxml2.XMLHTTP");
createXHR=function(){
return new ActiveXObject("Msxml2.XMLHTTP");
}
}catche(e){
try{
xhr =new ActiveXObject("Microsoft.XMLHTTP");
createXHR=function(){
return new ActiveXObject("Microsoft.XMLHTTP");
}
}catch(e){
createXHR=function(){
return null
}
}
}
}
}
8、高阶函数
函数当参数,把传入的函数做一个封装,然后返回这个封装的函数,达到更高程度的抽象。
var add = function(a, b){
return a+b;
}
function math(func, arr){
return func(arr[0], arr[1]);
}
math(add, [12, 21, 31]); //33
9、尾递归
function sum(n){
if (n === 1){
return 1;
}
return n + sum(n-1);
}
sum(4)
求值过程如下:
sum(4)
(4 + sum(3))
(4 + (3 + sum(2)))
(4 + (3 + (2 + sum(1))))
(4 + (3 + (2 + 1)))
(4 + (3 + 3))
(4 + 6)
10
普通递归时,内存需要记录调用的堆栈所处的深度和位置信息,在最底层计算返回值,再根据记录的信息,跳回上一层级计算,然后再跳回更高一层,依次运行,直到最外层的调用函数,cpu计算和内存消耗很多,而且当深度过大时,会出现堆栈溢出。
通过尾递归优化后的代码:
function sum(n, total){
if (n === 1){
return n + total;
}
return sum(n-1, n+total);
}
sum(5, 0) //10
求值过程如下:
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
1+14
15
整个过程是现行的,调用一次(x, total)后,会进入下一个栈,相关数据信息会跟随进入,不会放入堆栈保存。当计算完最后的值之后,直接返回到最上层的sum(5, 0)。这能有效的防止堆栈溢出。
尾部递归的的性能要高于传统纯函数的递归。
10、函数式编程中的闭包
function makePowerFn(power){
function powerFn(base){
return Math.pow(base, power);
}
return powerFn;
}
var square = makePowerFn(2);
square(3); //9
函数式编程其实是函数的种种技巧的拼接,但是函数式编程会充盈着大量的闭包,使用完需要释放,防止内存泄漏。
二、比较流行的函数式编程库
1、RxJS
RxJS(Reactive Extensions for JavaScript,JavaScript的响应式扩展),其函数响应式编程理念非常先进,虽然或许对于大部分应用环境来说,外部输入时间并不是太频繁,并不需要引入一个如此庞大的FRP(Functional Reactive Programming,函数响应式编程)体系,但我们也可以了解一下它的优秀特性。
在RxJS中,所有的外部输入(用户输入、网络请求等等)都被看做是一种“事件流”:
用户点击了按钮——>网络请求成功——>用户键盘输入——>某个定时事件发生,这种事件流特别适合处理游戏,上上下下,举个简单例子,下面这段代码会监听点击事件,每2次点击事件会产生一次事件响应:
var clicks = Rx.Observable
.fromEvent(document, 'click')
.bufferCount(2)
.subscribe(x => console.log(x)); //打印出前2次点击事件
2、Cycle.js
Cycle.js 是一个基于RxJS的框架,它是一个彻底的FRP理念的框架,和React一样支持virtual DOM,JSX语法,但现在似乎还没有看到大型应用经验。
本质的讲,它是在RxJS的基础上加入了对virtual DOM,容器和组件的支持,比如下面就有一个简单的“开关”按钮:
function main(source){
const sinks = {
DOM: sources.DOM.select('input').events('click')
.map(ev => ev.target.checked)
.startWith(false)
.map(toggled =>
<div>
<input type="checkbox" />Toggle me
<p>{toggled ? 'ON' : 'OFF'}</p>
</div>
)
};
return sinks;
}
const drivers = {
DOM: makeDOMDriver('#app')
}
run(main, drivers);
3、Underscore.js
Underscore是一个JavaScript工具库,它提供了一整套函数式编程的实用功能。但没有扩展任何JavaScript内置对象。它解决了这样的问题——“如果我面对一个空白HTML页面,并希望立即开始工作,我需要什么?”,它弥补了jQuery没有实现的功能,同时又是backbone必不可少的部分。
underscore提供了100多个函数,包括常用的map、filter、invoke等等,还有一些辅助函数,如:函数绑定,JavaScript模板功能,创建快速索引,以及强类型相等测试等。
4、lodash.js
loadash是一个具有一致接口、模块化、高性能等特性的JavaScript工具库,是underscore的fork,最初目的也是“一致的跨浏览器行为,并改善性能”。
lodash采用延迟计算,意味着我们的链式方法在显式或者隐式的value()调用之前是不会执行的,因此lodash可以进行shortcut(捷径)fusion(融合)这样的优化,通过合并链式大大降低迭代的次数,从而大大提升其执行性能。
就如同jQuery在全部函数前加全局的”$“一样,lodash使用全局的"_"来提供对工具的快速访问。
一个深层次查找属性值的示例:
var _ = require('lodash');
// Fetch the name of the first pet from each owner
var ownerArr = [{
"owner": "Colin",
"pets": [{"name":"dog1"}, {"name": "dog2"}]
}, {
"owner": "John",
"pets": [{"name":"dog3"}, {"name": "dog4"}]
}];
// Array's map method.
ownerArr.map(function(owner){
return owner.pets[0].name;
});
// Lodash
_.map(ownerArr, 'pets[0].name');
_.map 方法是对原生 map 方法的改进,使用字符串处理深层次嵌套属性的方式代替回调函数那些冗余的代码。
5、Ramda
Ramda是一个非常优秀的工具库,跟同类比更函数式,主要体现在一下原则:
- 1、Ramda里面提供的额函数全部是柯里化的意味着函数没有默认参数可选,从而减轻认知函数的难度。(即:所有方法都支持柯里化,所有多参数的函数,默认都可以单参数使用。)
如下例子:
var R = require('ramda');
var square = n => n * n;
// 写法一
R.map(square, [4, 8])
// 写法二
R.map(square)([4, 8])
// 或者
var mapSquare = R.map(square);
mapSquare([4, 8]);
上面代码中,写法一是多参数版本,写法二是柯里化以后的单参数版本。Ramda 都支持,并且推荐使用第二种写法。
2、Ramda推崇point free,简单的说就是使用简单函数组合实现一个复杂功能,而不是单独写一个函数操作临时变量。
3、Ramda有个非常好的参数占位符R._ ,大大减轻了函数在point free过程中参数位置的问题。
和underscore、lodash比较,Ramda要干净很多。
三、函数式编程的实际应用场景
- 易调试、热部署、并发
- 单元测试
1、易调试、热部署、并发
① 函数式编程中的每个符号都是const的,于是没有什么函数是有副作用的额。
② 函数式编程不需要考虑“死锁”(deadlock),因为它不修改变量,所以根本不存在“锁”线程的问题。
③ 函数式编程中的所有状态都是传给函数的参数,而参数都是存储在栈上的,这一特性,让软件的热部署变得十分简单。只要比较一下正在运行的代码和新的代码获得一个diff,然后用这个diff更新现有的代码,新代码的热部署就完成了。
2、单元测试
① 严格函数式编程的每一个符号都是对直接量或者表达式结果的引用,没有函数产生副作用。
②这是单元测试者的梦中仙境(wet dream),对被测试程序中的每个函数,你只需在意其参数,而不必考虑函数调用顺序,不用谨慎地设置外部状态,所有要做的就是传递代表了边际情况的参数。
四、总结
函数式编程不应该被视作灵丹妙药,相反,它应该被视为我们现有工具的一个很自然的补充——它带来了更高的可组合性,灵活性以及容错性。现代的JavaScript库已经开始尝试拥抱函数式编程的概念以获取这些优势。比如,Redux作为一种Flux的变种实现,核心理念也是状态机和函数式编程。
如果说面向对象编程降低复杂度是靠良好的封装、继承、多态以及接口的定义的话,那么函数式编程就是通过纯函数以及他们的组合、柯里化、Functor(函子)等技术来降低系统复杂度,而React、Rxjs、Cycle.js正是这种理念的代言。