这本书和之前讲的《JavaScript函数式编程指南》内容上有很大的重叠部分,就算是函数式编程的一个回顾吧。本书将的很多概念性的东西写的非常透彻,有必要再记录一遍,书也很薄(只有172页,图片看着厚很可能是出版社为了诱导消费者买书😂),值得一读!为了讲清楚函数式编程,书中的例子都是采用最精简的代码,并没有考虑代码的效率,甚至与我们常用的API稍微有点区别。
函数式编程简介
函数式编程是一种范式,我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数多次调用时仍然返回相同的结果。函数不会改变任何外部环境的变量,这将产生可缓存,可测试的代码库。
引用透明性:函数对于相同的输入都将返回相同的值。
纯函数:相同的输入返回相同输出的函数,该函数不应依赖任何外部变量,也不应改变任何外部变量。
高阶函数
高阶函数:接收函数作为参数或者返回函数作为输出的函数。
高阶函数举例(为了讲清楚内容,这里的函数都是低效的):
every(数组通过计算,若所有元素是否为true,则为true)
1
2
3
4
5
6
7
8
9const every = (arr,fn) => {
let result = true;
for(const value of arr)
result = result && fn(value)
return result
}
every([NaN, NaN, NaN], isNaN) // true
every([NaN, NaN, 1], isNaN) // falsesome(数组通过计算,只要有一个为true,那么结果为true)
1
2
3
4
5
6
7
8
9const some = (arr,fn) => {
let result = false;
for(const value of arr)
result = result || fn(value)
return result
}
some([NaN,NaN, 4], isNaN) // true
some([3,4, 4], isNaN) // falseunless(如果传入值是false时,则执行函数)
1
2
3
4const unless = (predicate,fn) => {
if(!predicate)
fn()
}times(从0开始迭代多少次)
1
2
3
4
5
6
7
8
9
10const times = (times, fn) => {
for (var i = 0; i < times; i++) fn(i);
}
// 打印0~99之间(共100个)的偶数
times(100, function(n) {
unless(n % 2, function() {
console.log(n, "is even");
});
});
闭包与高阶函数
闭包就是一个内部函数。如下,其中函数inner被称为闭包函数。
1 | function outer() { |
闭包可访问的作用域:
- 自身函数内的作用域;
- 全局作用域;
- 闭包所在的外部函数的作用域。
如下:
1 | let a = "全局作用域"; |
闭包可以记住上下文:
1 | var fn = (arg) => { |
上述代码需要注意closureFn函数已经在fn外部了,但是仍可以访问fn的变量outer,这个是闭包最重要的特征。
高阶函数举例(续):
tap(接收一个value返回一个函数,当函数执行时第一个参数是value)
1
2
3
4
5
6const tap = (value) =>
(fn) => (
typeof(fn) === 'function' && fn(value),
)
tap("fun")(value => console.log("value is " + value));// 打印 "value is fun"unary (将多参函数转化为一个参数的函数)
1
2
3
4
5
6
7const unary = (fn) =>
fn.length === 1
? fn
: (arg) => fn(arg)
['1', '2', '3'].map(parseInt);// 经典面试题 因为parseInt接受的第二个参数表示多少进制 导致最后返回的是 [1, NaN, NaN,]
['1', '2', '3'].map(unary(parseInt));// 返回 [1, 2, 3]once (函数只运行一次)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18const once = (fn) => {
let done = false;
return function () {
return done ? undefined : ((done = true), fn.apply(this, arguments))
}
}
var doPayment = once(() => {
console.log("Payment is done")
})
// 原函数执行,打印 "Payment is done"
doPayment()
// 原函数不执行
doPayment()memoized (函数记忆化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const memoized = (fn) => {
const lookupTable = {};
return (arg) => lookupTable[arg] || (lookupTable[arg] = fn(arg));
}
// 计算阶乘
var factorial = (n) => {
if (n === 0) {
return 1;
}
// This is it! Recursion!!
return n * factorial(n - 1);
}
// 记忆化阶乘函数
let fastFactorial = memoized(factorial);
// 下面打印 "Fast Factorial of 3 is 6"
console.log("Fast Factorial of 2 is", fastFactorial(3));
// 下面打印 "Fast Factorial of 3 is 6" 第二次的时候记忆化后的参数不再重新计算 直接返回6
console.log("Fast Factorial of 3 is", fastFactorial(3));
闭包与高阶函数
map(将数组转化为一个新的数组)
1
2
3
4
5
6
7
8
9
10const map = (array,fn) => {
let results = []
for(const value of array)
results.push(fn(value))
return results;
}
let squaredArray = map([1,2,3], (x) => x * x);
console.log(squaredArray); // [1, 4, 9]filter(过滤函数)
1
2
3
4
5
6
7
8
9
10const filter = (array,fn) => {
let results = []
for(const value of array)
(fn(value)) ? results.push(value) : undefined
return results;
}
// 实例:返回数组中的基数
filter([1, 2, 3, 4], (x)=>x % 2 === 1);// [1, 3]concatAll(数组扁平化,实际上就是我们常用的flatten,作用是将多个数组,合并成一个数组)
1
2
3
4
5
6
7
8
9const concatAll = (array) => {
let results = []
for(const value of array)
results.push.apply(results, value);
return results;
}
concatAll([[1, 2, 3], [3, 4, 5]]); // 结果为 [1, 2, 3, 3, 4, 5]reduce(累计计算)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20const reduce = (array, fn, initialValue) => {
let accumlator;
if(initialValue != undefined)
accumlator = initialValue;
else
accumlator = array[0];
if(initialValue === undefined)
for(let i=1;i<array.length;i++)
accumlator = fn(accumlator,array[i])
else
for(const value of array)
accumlator = fn(accumlator,value)
return accumlator
}
reduce([1,2,3,4,5],(acc,val) => acc + val,0); // 计算加法 返回 15
reduce([1,2,3,4,5],(acc,val) => acc * val,1); // 计算乘法 返回 120zip(合并两个指定的函数)
1
2
3
4
5
6
7
8
9
10const zip = (leftArr,rightArr,fn) => {
let index, results = [];
for(index = 0;index < Math.min(leftArr.length, rightArr.length);index++)
results.push(fn(leftArr[index],rightArr[index]));
return results;
}
zip([1, 2, 3],[4, 5, 6], (x, y) => x + y)); // 返回 [5, 7, 9]
柯里化与偏应用
一元函数:只接受一个参数的函数。
二元函数:只接受两个参数的函数。
变参函数:接受可变数量参数的函数。
如下:
1 | const identify = (x) => x; // 一元函数 |
柯里化:柯里化是把一个多参数函数转化为可嵌套的一元函数的过程。
一元柯里化:
1 | const curry = (binaryFn) => { |
多元柯里化:
1 | const curryN =(fn) => { |
回顾上面柯里化的例子,由于柯里化的参数是从左往右的,所以我们不得不定义一个转化函数setTimeoutWrapper将函数转化为多个嵌套函数,也就是curryN调用完curryN(setTimeoutWrapper)
再调用一下返回的函数,并传递参数1000
。那么能否提供一个函数,使得函数某几个参数始终都是相同的呢,这里就用到了偏函数,如下:
1 | const partial = function (fn,...partialArgs){ |
管道与组合
Unix中使用管道符号“|”来组合一些命令,使得前一个命令的输出是后一个命令的输入。如我们要统计某个文本文件中“World”出现的次数,可以使用下面的命令。
1 | cat test.txt | grep "World" | wc |
函数的组合:将一个函数的输出当成另一个函数的输入,最终把两者合并成一个函数。
1 | var compose = (a, b) => (c) => a(b(c)) |
组合多个函数:
1 | const composeN = (...fns) => |
上述组合函数参数是从右往左依次调用的,如果是从左往右那么就叫做管道了,也有称为序列。管道是组合的复制品,唯一修改的地方就是数据流的方向。
1 | const pipe = (...fns) => |
组合满足结合律:compose(f, compose(g, h)) === compose(compose(f, g), h)
函子
函子:函子是一个普通对象(在其他语言中可能是一个类),它实现了map函数,在遍历每个对象值的时候生成一个新的对象。
实际上数组就是函子!下面一步一步实现一个普通的函子:
1 | // 首先定义一个容器 由于需要new一个对象 所以这里没使用箭头函数 |
现在简绍一种新的函子,叫MayBe。MayBe函子是用来处理函数式编程空值问题的,实现如下:
1 | // 定义一个容器 跟上面一样的 就是改了一个名字 |
MayBe函子中每一个map函数都会执行,但是如果某一个map返回的是空,那么它后面的map函数的参数函数就都不会执行了,单map函数仍然会执行。
MayBe函子解决了空值的问题,Either函子解决或运算,Either函子实现如下:
1 | const Nothing = function(val) { |
Either函子在实际应用时,如果值在计算中不再参与计算的时候就使用Either.Nothing
否则使用Either.Some
。
Point函子:Point函子是函子的子集,它具有of方法。
我们写的MayBe函子和Either都实现了of方法,所以这两个都是Point函子。另外我们常用的数组,ES6也新增了of方法,所以它也是Point函子。
深入理解Monad
Monad也是一种函子,估计你看到Monad这个词你就头大了。此时你的内心:“卧槽!又要学习一个新的函子,真心学不动了,求别更新了!!!”
其实,函子这块就是纸老虎,各种名字天花乱坠,实际上都是很简单的,Monad也不例外,先看看Monad的定义。
Monad就是一个含有chain方法的函子。
是不是纸老虎,在说chain方法之前我们先简单的说一下另一个方法join,上面我们创建MayBe
函子以后最后都要调用.value
来返回真正的值,这里添加一个join方法,如果不为空的时候就返回函子的value
否则返回MayBe.of(null)
,如下:
1 | MayBe.prototype.join = function() { |
我们一般使用MayBe
的时候都会调用map函数的,大多数情况最后一个map调用完我们还会调用上面的join方法来获取value。为了简化最后这两步我们引入了chain方法:
1 | MayBe.prototype.chain = function(f){ |
这就是Monad的全部内容,没错就这!相信你已经理解的很深入了!
我们回顾一下这两节的内容:有map方法的对象就是函子,有of方法的函子就是Point函子,有chain方法的函子就是Monad函子。
Generator
本书最后一章介绍了ES6的Generator的使用,这里就简述一下:
1 | // 创建Generator(就是函数名和function之间加一个*) |