•纯函数
•函数的柯里化
•函数组合
•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的效能。