函数式编程常用核心概念

•纯函数

•函数的柯里化

•函数组合

•Point Free

•声明式与命令式代码

•核心概念

1.纯函数

什么是纯函数呢?

对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态的函数,叫做纯函数。

举个栗子:

1
2
3
4
5
var xs = [1,2,3,4,5];// Array.slice是纯函数,因为它没有副作用,对于固定的输入,输出总是固定的
xs.slice(0,3);
xs.slice(0,3);
xs.splice(0,3);// Array.splice会对原array造成影响,所以不纯
xs.splice(0,3);

  

2.函数柯里化

传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。

我们有这样一个函数checkage:

1
var min = 18; <br>var checkage = age => age > min;

  

这个函数并不纯,checkage 不仅取决于 age还有外部依赖的变量 min。 纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,柯里化优雅的函数式解决。

1
2
3
var checkage = min => (age => age > min);
 
var checkage18 = checkage(18); // 先将18作为参数,去调用此函数,返回一个函数age => age > 18;
1
checkage18(20);// 第二步,上面返回的函数去处理剩下的参数,即 20 => 20 > 18; return true;

  

 再看一个例子:
1
2
3
4
5
6
7
8
9
10
11
12
// 柯里化之前
function add(x, y) {
    return x + y;
}
add(1, 2) // 3
// 柯里化之后
function addX(y) {
    return function (x) {
        return x + y;
    };
}
addX(2)(1) // 3

  

  事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。

3.函数组合

为了解决函数嵌套过深,洋葱代码:h(g(f(x))),我们需要用到“函数组合”,我们一起来用柯里化来改他,让多个函数像拼积木一样。

1
2
3
4
5
const compose = (f, g) => (x => f(g(x)));
var first = arr => arr[0];
var reverse = arr => arr.reverse();
var last = compose(first, reverse);
last([1, 2, 3, 4, 5]); // 5

  

函数组合交换律,类似于乘法交换律:

4.Point Free

把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。
大家看一下下面的函数:
1
const f = str => str.toUpperCase().split(' ');
这个函数中,我们使用了 str 作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。
 
下面我们用函数组合去改造一下:
1
2
3
4
var toUpperCase = word => word.toUpperCase();
var split = x => (str => str.split(x));
var f = compose(split(' '), toUpperCase);
f("abcd efgh");

  

把一些对象自带的方法转化成纯函数,然后通过函数组合去调用,这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。是不是很方便!

5.声明式与命令式代码

在我们日常业务开发中,写的代码绝大多数都为命令式代码;

我们通过编写一条又一条指令去让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节。
而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。
1
2
3
4
5
6
7
//命令式
let CEOs = [];
for (var i = 0; i < companies.length; i++) {
    CEOs.push(companies[i].CEO)
}
//声明式
let CEOs = companies.map(c => c.CEO);

  

函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于我们的心智来说是极大的负担。

6.核心概念

下面我们再深入一下,大家注意好好理解吸收:

高阶函数

高阶函数,就是把函数当参数,把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。

1
2
3
4
5
6
7
8
9
//命令式
var add = function (a, b) {
    return a + b;
};
 
function math(func, array) {
    return func(array[0], array[1]);
}
math(add, [1, 2]); // 3

  

递归与尾递归

指函数内部的最后一个动作是函数调用。 该调用的返回值, 直接返回给函数。 函数调用自身, 称为递归。 如果尾调用自身, 就称为尾递归。 递归需要保存大量的调用记录, 很容易发生栈溢出错误, 如果使用尾递归优化, 将递归变为循环, 那么只需要保存一个调用记录, 这样就不会发生栈溢出错误了。通俗点说,尾递归最后一步需要调用自身,并且之后不能有其他额外操作。
1
2
3
4
5
6
7
8
9
10
// 不是尾递归,无法优化
function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
}
 
function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
//ES6强制使用尾递归

  

我们看一下递归和尾递归执行过程:

递归:

1
2
3
4
function sum(n) {
    if (n === 1) return 1;
    return n + sum(n - 1);
}

  

1
2
3
4
5
6
7
8
9
10
sum(5)
(5 + sum(4))
(5 + (4 + sum(3)))
(5 + (4 + (3 + sum(2))))
(5 + (4 + (3 + (2 + sum(1)))))
(5 + (4 + (3 + (2 + 1))))
(5 + (4 + (3 + 3)))
(5 + (4 + 6))
(5 + 10)
15 // 递归非常消耗内存,因为需要同时保存很多的调用帧,这样,就很容易发生“栈溢出”

  

尾递归

1
2
3
4
5
6
function sum(x, total) {
    if (x === 1) {
        return x + total;
    }
    return sum(x - 1, x + total);
}

  

1
2
3
4
5
6
sum(5, 0)
sum(4, 5)
sum(3, 9)
sum(2, 12)
sum(1, 14)
15

  

整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的sum(5,0)。这能有效的防止堆栈溢出。 在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能。
posted @ 2019-08-15 16:39  勒布朗_詹姆斯  阅读(420)  评论(0编辑  收藏  举报