一.函数式编程相关概念

函数式编程

  • 概念: 函数式编程是与面向对象编程和过程式编程并列的编程范式。
    通俗的来说函数式编程就是对于给定的输入,不管你调用该函数多少次,永远返回同样的结果。
    在函数式编程中,函数就是一个管道(pipe)。这头进去一个值,那头就会出来一个新的值,没有其他作用。
  • 特点:
  1. 函数是第一等公民.和其它的基本数据类型一样, 可以当参数传递,可以赋值给变量等等.
    (JavaScript就是这样.混合范式, 命令式,面向对象,函数式)
  2. 强调将计算过程分解成可复用的函数
  3. 只有纯的,没有副作用的函数,才是合格的函数,即纯函数
  • 究竟有什么好处?
  • 代码易维护
  • 易调试
  • 单元测试

纯函数

  • 概念: 一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。
  • 举例:
const a = 1
 const foo = (b) => a + b
 foo(2) // => 3   这里的foo()不是一个纯函数, 因为受到外部变量a的影响
- 在JS中,像toUpperCase(),slice()这样固定输入导致固定输出的即函数.Math.random(), Date.now()非纯函数
- 什么叫做副作用?

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。如:
更改文件系统
往数据库插入记录
发送一个 http 请求
可变数据
获取用户输入
DOM 查询
访问系统状态

  • 为什么要坚持用纯函数?
  • 可移植性/自文档化. 纯函数是完全自给自足的,依赖于自身.

高阶函数

  • 概念: 可以把函数作为参数,或者是可以将函数作为返回值的函数。
  • 举例
const arr = [9, 5, 2, 7]
 const foo = function (x) {
   return x * x
 }
 const result = arr.map(foo)
 console.log(result)   // [ 81, 25, 4, 49 ]

map, filter, reduce, sort等等接收函数作为参数的函数其实都是高阶函数.

  • 引申
  • 高阶函数应用–高阶组件, 被更多的应用于React中
  • 一个函数就是一个组件, React中常提的高阶组件其实也是高阶函数,只是传入的参数变成了React组件,并返回一个新的组件,两种高阶组件的形式: 属性代理(props)和反向继承. 高阶组件的主要功能是封装并分离组件的通用逻辑,让通用逻辑在组件间更好地被复用,高阶组件的这种实现方式,本质上是一个装饰者设计模式。

函数柯里化和合成

  • 柯里化: 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。柯里化其实是高阶函数的一种实现,即低阶函数转换为高阶函数,以期望把一个多参数的函数,转化为单参数函数。
// 实现函数add(2,5) add(2)(5)都输出7?
function add(x, y) {
  if (arguments.length === 1) {
    return function (z) { return x + z }
  } else {
    return x + y
  }
}
console.log(add(2)(5));   // 7
console.log(add(2, 5));   // 7
// 柯里化	把多参数的函数,转化为单参数函数
function sum(x, y) {
 	 return x + y
}
function currySum(y) {
	  return function (x) {
	    return x + y
	  }
}
console.log(sum(1, 2))         // 3
console.log(currySum(2)(1))    // 3
  • 柯里化的"预加载"能力, 通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。从某种意义上来讲,这是一种对参数的复用
let checkage = min => (age => age > min);
let checkage18 = checkage(18);
checkage18(20);	// =>true
  • 函数的合成(compose): 如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做"函数的合成"(compose)。更好的独立性
//  步骤拆分实现(x + 3) * 3
function add(x) {
  	return x + 3
}
function multiply(x) {
 	 return x * 3
}
let result = multiply(add(2))
console.log(result)   // 15
// 合成
function compose(f, g) {
	  return function (x) {
	    return f(g(x))
	  }
}
let composeFoo = compose(multiply, add)
console.log(composeFoo(2))  // 15

补充:完整的柯里化和合成工具函数参考lodash工具库 https://www.lodashjs.com/

命令式与声明式

  • 命令式代码:命令“机器”如何去做事情(how),这样不管你想要的是什么(what),它都会按照你的命令实现。
  • 声明式代码:告诉“机器”你想要的是什么(what),让机器想出如何去做(how)。
    与命令式不同,声明式意味着我们要写表达式,而不是一步一步的指示。日常编码习惯就是声明式
// 命令式
const makes = [];
for (let i = 0; i < cars.length; i++) {
  	makes.push(cars[i].make);
}
// 声明式
let makes = cars.map(car => car.make);

函子(Functor)

  • 理解函子: 它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。

任何具有map方法的数据结构,都可以当作函子的实现。

class Functor {
	  constructor(val) { 
	    this.val = val; 
	  }

	  map(f) {
	    return new Functor(f(this.val));
	  }
}
// 不使用new关键字, 函数式编程一般约定,函子有一个of方法,用来生成新的容器。
Functor.of = function(val) {
  	return new Functor(val);
};

二.Monad函子与Promise

Monad函子(单子)

  • 函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
  • Monad 函子的作用是,总是返回一个单层的函子。它有一个flatMap方法,与map方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。
class Monad extends Functor {
	  join() {
	    return this.val;
	  }
	  flatMap(f) {
	    return this.map(f).join();
	  }
}

Promise

  • Promise用于解决层层嵌套导致的回调地狱
  • 在每次 Resolve 一个 Promise 时,我们需要判断两种情况:
    如果被 Resolve 的内容仍然是 Promise(即所谓的 thenable),那么递归 Resolve 这个 Promise。
    如果被 Resolve 的内容不是 Promise,那么根据内容的具体情况(如对象、函数、基本类型等),去 fulfill 或 reject 当前 Promise。
  • Monad函子的flatMap的能力就是能够递归地把容器一层层拆开,直接取出最里面装着的值。在这里的容器指的是Promise, flatMap还有另外一个名字—chain(链)
  • 并没有什么确切的说法,说Promise的实现就是Monad函子,只是递归思想相似,拿来对比学习

三.函数式编程在JS中的其它应用

  • 闭包
  • 各种API方法
  • 待补充…(学习的还不够深入, 后续会继续完善)

四.总结与参考链接

  • 函数对于外部状态的依赖是造成系统复杂性大大提高的主要原因,让函数尽可能地纯净,进一步降低系统复杂度.当然,所有的数据都是不可以改变的,也会造成很严重的问题,比如占据运行资源卡顿等.学习过程中,看到很多人提到"没有银弹"这个概念, 旨在说明在软件开发过程里是没有万能的终杀性武器的,只有各种方法综合运用,才是解决之道。
  • 待补充的一些点: 尾递归优化, 惰性求值, pointfree, Applicative, 其它函子