快速入门函数式编程——以Javascript为例

此外,函数式编程要求我们编写纯的、确定性的函数,这些函数不太可能有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。

函数式编程的三大原则(重要!!!)

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]

如果它是不纯的,那么整个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)

通常,它们用于给函数添加功能(类似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

这就是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)),满足复合函数的性质。

总结

随着ES6的引入,JavaScript提供了比以前更好的函数式编程体验。

关键概念扫盲

1.函数式编程好处都有啥?谁说对了就给他!美国,圣地亚哥!

函数式编程可以帮助我们避免bug并轻松理解代码。

2.ES6是啥玩意?

ES6全称ECMAScript 6是一个新版本的JavaScript,介4里妹有用过的船新版本,挤需体验三番钟,里奏会....ES6包含了许多贼酷炫的新特性像箭头函数, 常量, 害有解构赋值运算符等等等。