该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog命令式编程和声明式编程拿泡茶这个事例进行区分命令式编程和声明式编程命令式编程1.烧开水(为第一人称)2.拿个茶杯3.放茶叶4.冲水声明式编程1.给我泡杯茶(为第二人称)举个 demo// 命令式编程const convert = function(arr) { const result = [] for (let i = 0; i < arr.length; i++) { result[i] = arr[i].toLowerCase() } return result}// 声明式编程const convert = function(arr) { return arr.map(r => r.toLowerCase())}什么是函数式编程函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。函数式编程可以用简单如交换律、结合律、分配律的数学之法来帮我们简化代码的实现。它具有如下一些特性:纯粹性: 纯函数不改变除当前作用域以外的值;// 反面示例let a = 0const add = (b) => a = a + b // 两次 add(1) 结果不一致// 正确示例const add = (a, b) => a + b数据不可变性: Immutable// 反面示例const arr = [1, 2]const arrAdd = (value) => { arr.push(value) return arr}arrAdd(3) // [1, 2, 3]arrAdd(3) // [1, 2, 3, 3]// 正面示例const arr = [1, 2]const arrAdd = (value) => { return arr.concat(value)}arrAdd(3) // [1, 2, 3]arrAdd(3) // [1, 2, 3]在后记 1 中对数组字符串方法是否对原值有影响作了整理函数柯里化: 将多个入参的函数转化为一个入参的函数;const add = a => b => c => a + b + cadd(1)(2)(3)偏函数: 将多个入参的函数转化成两部分;const add = a => (b, c) => a + b + cadd(1)(2, 3)可组合: 函数之间能组合使用const add = (x) => x + xconst mult = (x) => x * xconst addAndMult = (x) => add(mult(x))柯里化(curry)如下是一个加法函数:var add = (a, b, c) => a + b + cadd(1, 2, 3) // 6假如有这样一个 curry 函数, 用其包装 add 函数后返回一个新的函数 curryAdd, 我们可以将参数 a、b 进行分开传递进行调用。var curryAdd = curry(add)// 以下输出结果都相同curryAdd(1, 2, 3) // 6curryAdd(1, 2)(3) // 6curryAdd(1)(2)(3) // 6curryAdd(1)(2, 3) // 6动手实现一个 curry 函数核心思路: 若传进去的参数个数未达到 curryAdd 的个数,则将参数缓存在闭包变量 lists 中:function curry(fn, …args) { const length = fn.length let lists = args || [] let listLen return function (…_args) { lists = […lists, …_args] listLen = lists.length if (listLen < length) { const that = lists lists = [] return curry(fn, …that) } else if (listLen === length) { const that = lists lists = [] return fn.apply(this, that) } }}代码组合(compose)现在有 toUpperCase、reverse、head 三个函数, 分别如下:var toUpperCase = (str) => str.toUpperCase()var reverse = (arr) => arr.reverse()var head = (arr) => arr[0]接着使用它们实现将数组末位元素大写化输出, 可以这样做:var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr)))reverseHeadUpperCase([‘apple’, ‘banana’, ‘peach’]) // “PEACH"此时在构建 reverseHeadUpperCase 函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose 函数让使用者更加友好的使用呢? 类似如下形式:var reverseHeadUpperCase = compose(toUpperCase, head, reverse)reverseHeadUpperCase([‘apple’, ‘banana’, ‘peach’]) // “PEACH"此外 compose 函数符合结合律, 我们可以这样子使用:compose(compose(toUpperCase, head), reverse)compose(toUpperCase, compose(head, reverse))以上两种写法与 compose(toUpperCase, head, reverse) 的效果完全相同, 都是依次从右到左执行传参中的函数。此外 compose 和 map 一起使用时也有相关的结合律, 以下两种写法效果相等compose(map(f), map(g))map(compose(f, g))动手实现一个 compose 函数代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。var compose = (…args) => (initValue) => args.reduceRight((a, c) => c(a), initValue)范畴论范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:学习函数式编程就是学习各种函子的过程。函数式编程中, 函子(Functor) 是实现了 map 函数的容器, 下文中将函子视为范畴,模型可表示如下:class Functor { constructor(value) { this.value = value } map(fn) { return new Functor(fn(this.value)) }}但是在函数式编程中, 要避免使用 new 这种面向对象的编程方式, 取而代之对外暴露了一个 of 的接口, 也称为 pointed functor。Functor.of = value => new Functor(value)Maybe 函子Maybe 函子是为了解决 this.value 为 null 的情形, 用法如下:Maybe.of(null).map(r => r.toUpperCase()) // nullMaybe.of(’m’).map(r => r.toUpperCase()) // Maybe {value: “M”}实现代码如下:class Maybe { constructor(value) { this.value = value } map(fn) { return this.value ? new Maybe(fn(this.value)) : null }}Maybe.of = value => new Maybe(value)Either 函子Either 函子 是为了对应 if…else… 的语法, 即非左即右。因此可以将之拆分为 Left 和 Right 两个函子, 它们的用法如下:Left.of(1).map(r => r + 1) // Left {value: 1}Right.of(1).map(r => r + 1) // Right {value: 2}Left 函子实现代码如下:class Left { constructor(value) { this.value = value } map(fn) { return this }}Left.of = value => new Left(value)Right 函子实现代码如下(其实就是上面的 Functor):class Right { constructor(value) { this.value = value } map(fn) { return new Right(fn(this.value)) }}Right.of = value => new Right(value)具体 Either 函数只是对调用 Left 函子 或 Right 函子 作一层筛选, 其接收 f、g 两个函数以及一个函子(Left or Right)var Either = function(f, g, functor) { switch(functor.constructor) { case ‘Left’: return f(functor.value) case ‘Right’: return g(functor.value) default: return f(functor.value) }}使用 demo:Either((v) => console.log(’left’, v), (v) => console.log(‘def’, v), left) // left 1Either((v) => console.log(‘rigth’, v), (v) => console.log(‘def’, v), rigth) // rigth 2Monad 函子函子会发生嵌套, 比如下面这样:Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } }Monad 函子 对外暴露了 join 和 flatmap 接口, 调用者从而可以扁平化嵌套的函子。class Monad { constructor(value) { this.value = value } map(fn) { return new Monad(fn(this.value)) } join() { return this.value } flatmap(fn) { return this.map(fn).join() }}Monad.of = value => new Monad(value)使用方法:// joinMonad.of(Monad.of(1).join()) // Monad { value: 1 }Monad.of(Monad.of(1)).join() // Monad { value: 1 }// flatmapMonad.of(1).flatmap(r => r + 1) // 2Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。后记 1: 数组字符串方法小结(是否对原值有影响)不会对原数组有影响的方法slicevar test = [1, 2, 3]var result = test.slice(0, 1)console.log(test) // [1, 2, 3]console.log(result) // [1]concatvar test = [1, 2, 3]var result = test.concat(4)console.log(test) // [1, 2, 3]console.log(result) // [1, 2, 3, 4]对原数组有影响的方法splice(这个需要特别记一下)var test = [1, 2, 3]var result = test.splice(0, 1)console.log(test) // [2, 3]console.log(result) // [1]sortvar arr = [2, 1, 3, 4]arr.sort((r1, r2) => (r1 - r2))console.log(arr) // [1, 2, 3, 4]reversevar test = [1, 2, 3]var result = test.reverse()console.log(test) // [3, 2, 1]console.log(result) // [3, 2, 1]push/pop/unshift/shiftvar test = [1, 2, 3]var result = test.push(4)console.log(test) // [1, 2, 3, 4]console.log(result) // 4不会对原字符串造成影响的方法substr/substring/slice// substrvar test = ‘abc’var result = test.substr(0, 1)console.log(test) // ‘abc’console.log(result) // a// substringvar test = ‘abc’var result = test.substring(0, 1)console.log(test) // ‘abc’console.log(result) // a// slicevar test = ‘abc’var result = test.slice(0, 1)console.log(test) // ‘abc’console.log(result) // a参考mostly-adequate-guideJavaScript 专题之函数柯里化函数式编程入门教程