函数式编程常用核心概念
•纯函数
•函数的柯里化
•函数组合
•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的效能。

浙公网安备 33010602011771号