在文章之前,先和大家讲一下对于函数式编程(Functional Programming, aka. FP)的理解(下文我会用FP指代函数式编程):
FP需要保证函数都是纯净的,既不依赖外部的状态变量,也不产生副作用。基于此前提下,那么纯函数的组合与调用,在时间顺序上就不会产生依赖,改变多个函数的调用顺序也不必担心产生问题,因此也会消灭许多潜在的bug。
函数必须有输入输出。如果一个函数缺乏输入或输出,那么它其实是一段处理程序procedure而已。
函数尽可能的保持功能的单一,如果一个函数做了多件事情,那么它理论上应当被拆分为多个函数。
FP的意义之一就是,在适当的时机使用声明式编程,抽象了程序流的控制与表现,从理解和维护的角度上会胜于命令式编程。
FP是一种范式,但并不意味这和OOP(面向对象编程)冲突,两者当然是可以和谐共存的。个人认为 React 其实就是一个很好的栗子~
Javascript的函数一等公民以及闭包的特性,决定了Javascript的确是适合施展FP的舞台
理解闭包
闭包对于 Javascript 来说,当然十分重要。然而对于函数式编程来说,这更加是必不可少的,必须掌握的概念,闭包的定义如下:
Closure is when a function remembers and accesses variables from outside of its own scope, even when that function is executed in a different scope.
相信大部分同学都对闭包有不错的理解,但是由于对FP的学习十分重要。接下来我还是会啰嗦的带大家过一遍。闭包就是能够读取其他函数内部变量的函数
简单示例如下
// Closure demo function cube(x) { let z = 1; return function larger(y) { return x * y * z++; }; } const makeCube = cube(10); console.log(makeCube(5)); // 50 console.log(makeCube(5)); // 100
那么有没有想过在函数makeCube,或者也可以说是函数larger是怎么记住原本不属于自己作用域的变量x和z的呢?在控制台查看makeCube.prototype,点开会发现原来是有个[[Scopes]]这个内置属性里的Closure(cube)记住了函数larger返回时记住的变量x和z。如果多嵌套几层函数,也会发现多几个Closure(name)在[[Scopes]]的Scopes[]数组里,按序查找变量。
再看下图测试代码:
function cube(x) { return function wrapper(y) { let z = 1; return function larger() { return x * y * z++; }; } } const makeCubeY = cube(10); const makeCube = makeCubeY(5); const $__VAR1__ = '1. This var is just for test.'; let $__VAR2__ = '2. This var is just for test.'; var $__VAR3__ = '3. This var is just for test.'; console.log(makeCubeY.prototype, makeCube.prototype); console.log(makeCube()); // 50 console.log(makeCube()); // 100
打印makeCubeY.prototype:
打印makeCube.prototype:
通过这几个实验可以从另一个角度去理解Javascript中闭包,一个闭包是怎么去查找不是自己作用域的变量呢?makeCube函数分别从[[Scopes]]中的Closure(wrapper)里找到变量y、z,Closure(cube)里找到变量x。至于全局let、const声明的变量放在了Script里,全局var声明的变量放在了Global里。
在学习FP前,理解闭包是尤为重要的~ 因为事实上大量的FP工具函数都使用了闭包这个特性。
工具函数
unary
const unary = fn => arg => fn(arg);
一元函数,应用于当只想在某个函数上传递一个参数情况下使用。尝试考虑以下场景:
console.log(['1', '2', '3'].map(parseInt)); // [1, NaN, NaN] console.log(['1', '2', '3'].map(unary(parseInt))); // [1, 2, 3]
parseInt(string, radix)接收两个参数,而map函数中接收的回调函数callback(currentValue[, index[, array]]),第二个参数是index,此时如果parseInt的使用就是错误的。当然除了Array.prototype.map,大量内置的数组方法中的回调函数中都不止传递一个参数,如果存在适用的只需要第一个参数的场景,unary函数就发挥了它的价值,无需修改函数,优雅简洁地就接入了。(对于unary函数,fn就是闭包记忆的变量数据)
identity
const identity = v => v;
有同学会看到identity函数会觉得莫名其妙?是干嘛的?我第一眼看到也很迷惑?但是考虑以下场景:
console.log([false, 1, 2, 0, '5', true].filter( identity )); // [1, 2, "5", true] console.log([false, 0].some( identity )); // false console.log([-2, 1, '3'].every( identity )); // true