函数式编程是在不改变状态和数据的情况下使用表达式和函数来编写程序的一种编程范式。
通过遵守这种范式,我们能够编写更清晰易懂、更能抵御bug的代码。这是通过避免使用流控制语句(for、while、break、continue、goto)来实现的,这些语句会使代码更难理解。此外,函数式编程要求我们编写纯的、确定性的函数,这些函数不太可能有bug。
在开始函数式编程之前,我们需要了解一下什么是纯函数和非纯函数。
纯函数就是对于固定的输入一定会有固定的输出。而且,它们对外界没有任何副作用。
const add = (a, b) => a + b;
这里,add就是一个纯函数。这是因为,对于固定值a和b,输出总是相同的。
const SECRET = 2019;
const getId = (a) => SECRET * a;
getId就不是一个纯函数。因为它使用了全局变量SECRET。如果SECRET发生变化,getId函数将为相同的输入返回不同的值。因此,它不是一个纯函数。
let id_count = 0;
const getId = () => ++id_count;
这也是一个非纯函数,原因如下:(1)它使用一个非局部变量来计算它的输出,(2)它修改了外部世界中的一个变量,产生了副作用。
getId()——>1
getId()——>2
getId()——>3
???每次调用结果都不一样?我们如果调试这段代码可能会五脸蒙B。
id_count的当前值是多少?哪些其他函数正在修改id_count?是否有其他函数依赖于id_count?
为了避免这些不确定性,使得代码更健壮,所以我们要使用纯函数。
函数式编程的三大原则(重要!!!)
1.不改变数据
2.使用纯函数:固定的输入总能得到固定的输出,而且妹有副作用
插播一下 副作用是啥玩意来着?
大家如果学过C语言的话肯定接触过
比如a=b= 50
从C语言的角度来讲,目的是对表达式求值(这个语句的结果是50)。
但是使用我们写这个赋值表达式根本目的就是使用其副作用(C语言本身目的——运算 之外的作用效果):将a和b变量的值设置为50。
3.使用表达式不使用语句
"表达式"(expression)是一个单纯的运算过程,总是有返回值;
"语句"(statement)是执行某种操作,没有返回值。
也就是说,每一步都是单纯的运算,而且都有返回值。
现在看完这三点可能不太懂,但是对照下面的例子相信你就能理解了。
JavaScript中的函数式编程
JavaScript有const关键字,它非常适合函数式编程,因为不会改变数据。
让我们来康康JavaScript提供的一些纯函数。
Filter
见名知义,就是对数组进行过滤。
array.filter(condition);
这个condition(过滤条件)是一个函数,它拿到数组的每一项然后决定是否留下它。(通过返回true)
const filterEven = x => x%2 === 0; [1, 2, 3].filter(filterEven); // [2]
注意,filterEven是一个纯函数。如果它是不纯的,那么整个filter函数也不能称之为纯函数了。
Map
map将数组的每一项传给一个函数,然后根据函数的返回值创建一个新数组。
array.map(mapper)
mapper(映射器)就是拿到数组的每一项然后返回一个值作为新数组的元素。
const double = x => 2 * x; [1, 2, 3].map(double); // [2, 4, 6]
Reduce
reduce直译是减少,为什么叫减少呢,是因为它把整个数组变成了一个值。
array.reduce(reducer);
reducer是一个函数,它根据已有的结果和数组中的下一项返回一个新值(新值继续作为已有的结果)。
const sum = (accumulatedSum, arrayItem) => accumulatedSum + arrayItem [1, 2, 3].reduce(sum); // 6
我们可以发现这个reduce完成的就是数组的累加,那过程是怎样的呢?
第一次已有的结果默认为第一个元素,因此第一次sum函数的参数是(1,2),返回值为3,
3又作为accumulatedSum,第二次sum函数调用即为(3,3),返回值为6.
concat
concat在原有数组基础上拼接新的元素以创建新数组。它不同于push(),因为push()会改变数据(向原来的数组添加元素),从而成为非纯函数。
[1, 2].concat([3, 4]) // [1, 2, 3, 4]
用ES6的话,可以用解构赋值运算符...实现。
[1, 2, ...[3, 4]]
简单吧,记住这个语法哦,很常用。
Object.assign
Object.assign会拷贝给定对象的值给新对象。由于函数式编程是基于不可变数据的,所以我们使用它,来根据现有对象创建新对象。
const obj = {a : 2}; const newObj = Object.assign({}, obj); newObj.a = 3; obj.a; // 2
简写方法:(ES6的解构赋值又来了)
const newObj = {...obj};
创建我们自己的纯函数
我们也可以创建自己的纯函数。让我们来做一个复制一个字符串n次的例子。
const duplicate = (str, n) =>
n < 1 ? '' : str + duplicate(str, n-1);
duplicate('bokeyuan!', 3) // bokeyuan!bokeyuan!bokeyuan!
高阶函数(Higher-order Functions, 后文简称HOF)
HOF是接受函数作为参数或返回函数的函数。通常,它们用于给函数添加功能(类似Java的AOP或Python的装饰器,有学过的小伙伴可以类比一下)。
const withLog = (fn) => { return (...args) => { console.log(`calling ${fn.name}`); return fn(...args); }; };
我们创建了一个withLog HOF,它接受一个函数并返回一个函数,该函数在传入的函数运行之前会记录一条消息。
const add = (a, b) => a + b; const addWithLogging = withLog(add); addWithLogging(3, 4); // calling add // 7
withLog HOF也可以与其他函数一起使用,它不会产生任何冲突,也不会编写额外的代码。这就是HOF的美丽之处。
const addWithLogging = withLog(add); const hype = s => s + '!!!'; const hypeWithLogging = withLog(hype); hypeWithLogging('Sale'); // calling hype // Sale!!!
也可以简单点直接调用HOF的返回值。
withLog(hype)('Sale'); // calling hype // Sale!!!
柯里化(Currying)
柯里化意味着将一个多参数的函数分解成高阶函数。看完这句话目测大家内心又是崩溃的。
我们还拿add函数来举栗子。
const add = (a, b) => a + b;
当我们要把它柯里化时,我们改写它,将这一个函数的多个参数分布到多个函数,如下所示。
const add = a => { return b => { return a + b; }; }; add(3)(4); // 7
柯里化的好处就是记忆性。我们可以在函数调用中记忆某些参数,以便以后可以重用它们,而不需要重新计算或者写冗余代码。
举个栗子来看一下
add(4, getOffsetNumber()); add(6, getOffsetNumber()); add(10, getOffsetNumber());
我们假设调用getOffsetNumer()这个函数非常消耗资源,因此上面的代码,第一,造成了耗资源操作的多次重新计算,第二,也产生了冗余代码,一样的调用写三遍。
而如果我们采用柯里化之后的add函数,偶们来看一下。
const addOffset = add(getOffsetNumber()); addOffset(4); addOffset(6);
这个好处就不用我说,不言自明了吧。
拓展
我们还可以进一步优化柯里化函数的写法,使其看起来更简洁。
这是因为每一层的函数调用都是一个单行返回语句。因此,我们可以在ES6中使用箭头函数重构它,如下所示。
const add = a => b => a + b;
(这块看不懂没关系,语法不重要,重在理解柯里化的意义)
复合函数(Composition)
高中数学俺们都学过,复合函数函数套函数,就是把一个函数的输出传递给另一个函数作为输入,从而产生一个组合输出。
那我们讲一下复合函数在函数式编程里面的应用。我们先准备点素材。
第一个函数叫 range,接收俩参数a和b,产生一个数组,里面内容是从a到b的每个数。
const range = (a, b) => a > b ? [] : [a, ...range(a+1, b)];
然后我们再来一个函数叫multiply,接收一个数组,然后把里面所有元素累乘起来。
const multiply = arr => arr.reduce((p, a) => p * a);
然后我们利用这俩玩意去实现阶乘。
const factorial = n => multiply(range(1, n));
factorial(5);
// 120
factorial(6);
// 720
factorial这个函数就类似于f(x) = g(h(x)),满足复合函数的性质。
总结
俺们学习了纯函数和非纯函数、函数式编程、新的JavaScript特性以及函数式编程中的一些关键概念。
我们希望这篇文章能激起你对函数式编程的兴趣,然后泥,多在代码中尝试运用它。
函数式编程是一种经过充分研究的健壮的计算机程序编写范例。随着ES6的引入,JavaScript提供了比以前更好的函数式编程体验。
关键概念扫盲
1.函数式编程好处都有啥?谁说对了就给他!美国,圣地亚哥!
函数式编程确保了代码中更简单的流程控制,并避免了变量和状态更改可能会给代码带来的一些不确定性,给我们造成惊喜,哦不,惊吓。函数式编程可以帮助我们避免bug并轻松理解代码。
2.ES6是啥玩意?
ES6全称ECMAScript 6是一个新版本的JavaScript,介4里妹有用过的船新版本,挤需体验三番钟,里奏会....ES6包含了许多贼酷炫的新特性像箭头函数, 常量, 害有解构赋值运算符等等等。