JS函数式编程范式

头等函数

当一门编程语言的函数可以被当作变量一样用时,则称这门语言拥有头等函数
JS中的函数可以:

  • 作为函数的参数
  • 作为函数的返回值
  • 赋值给一个变量

高阶函数

一个返回另外一个函数的函数被称为高阶函数

function sayHello() {
   return function() {
      console.log("Hello!");
   }
}

纯函数

相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
优点:

  • 可缓存: 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
  • 可测试: 纯函数让测试更方便
  • 并行处理:在多线程环境下并行操作共享的内存数据很可能会出现意外情况,纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数 (Web Worker)

副作用

如果函数依赖于外部 的状态就无法保证输出相同,就会带来副作用。
副作用来源:

  • 外部变量
  • 配置文件
  • 数据库
  • 获取用户的输入
  • ...
// 不纯的
let mini = 18
function checkAge (age) {
  return age >= mini
}
// 纯的(有硬编码,后续可以通过柯里化解决) 
function checkAge (age) {
let mini = 18
  return age >= mini
}

闭包(Closure)

可以在另一个作用域中调用一个函数A的内部函数B并访问到A函数的作用域中的成员

// 函数作为返回值 
function A() {
  // A作用域中的变量
  let msg = 'Hello function'
  // B
  return function () {
      console.log(msg) 
  }
}
const fn = A()
fn()

柯里化(Curry)

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,返回结果

  • 可以给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的'缓存'
  • 让函数变的更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

案例1:自己柯里化
普通纯函数(多元函数)

function checkAge (min, age) {
  return age >= min
}
checkAge(18, 24)
checkAge(18, 20)
checkAge(20, 30)

柯里化(一元函数)

function checkAge (min) {
  return function (age) {
    return age >= min
} }

// ES6 写法
let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18) 
let checkAge20 = checkAge(20)
checkAge18(24)
checkAge18(20)

案例2:使用lodash库中的curry函数柯里化函数

const _ = require('lodash') // 要柯里化的函数
function getSum (a, b, c) {
  return a + b + c
}
// 柯里化后的函数
let curried = _.curry(getSum) // 测试
curried(1, 2, 3) curried(1)(2)(3)
curried(1, 2)(3)

模拟实现 lodash 中的 curry 方法

function curry (func) {
  return function curriedFn(...args) {
    // 判断实参和形参的个数
    if (args.length < func.length) {
      return function () {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}

组合函数(Compose)

如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数,这个函数就是组合函数

案例1:自己实现组合函数

// 组合函数
function compose (f, g) {
  return function (x) {
    return f(g(x))
} }

function first (arr) {
  return arr[0]
}

function reverse (arr) { 
  return arr.reverse()
}

// 从右到左运行
let last = compose(first, reverse) 
console.log(last([1, 2, 3, 4]))

案例2:使用lodash中flowRight函数实现组合函数

  • flow是从左到右运行
  • flowRight是从右到左运行,使用的更多一些
const _ = require('lodash')
const toUpper = s => s.toUpperCase() 
const reverse = arr => arr.reverse() 
const first = arr => arr[0]
const f = _.flowRight(toUpper, first, reverse) 
console.log(f(['one', 'two', 'three']))

模拟实现lodash的flowRight函数()

// 多函数组合
function compose (...fns) {
  return function (value) {
      return fns.reverse().reduce(function (acc, fn) {
          return fn(acc)
      }, value)
  } 
}
// ES6
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

Point Free

我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数

案例1:使用 Point Free 的模式,把单词中的首字母提取并转换成大写

const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '), fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web')) // => W. W. W

函子(Functor)

是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map 方法可以运行一个函数对值进行处理(变形关系)
最基本的函子实现如下:

// 一个容器,包裹一个值 
class Container {
    // of 静态方法,可以省略 new 关键字创建对象 
    static of(value) {
        return new Container(value) 
    }
    constructor(value) { 
        this._value = value
    }    
    // map 方法,传入变形关系,将容器里的每一个值映射到另一个容器 
    map(fn) {
        return Container.of(fn(this._value)) 
    }
}
// 测试 
Container.of(3)
.map(x => x + 2) 
.map(x => x * x)

但是如果传入了null或undefined,就会可能会发生异常,从而产生副作用

Container.of(null)
.map(x => x.toUpperCase())
// TypeError: Cannot read property 'toUpperCase' of null

MayBe 函子

Maybe函子和Container类似,只不过Maybe函子内部对输入值做了判断处理,防止异常发生

class Maybe {
    static of(value) {
        return new Maybe(value)
    }
    
    constructor(value) {
        this._value = value
    }

    isNothing() {
        return this._value === null || this._value === undefined
    }

    // 内部处理异常情况,控制副作用
    map(fn) {
        return this.isNothing() ? this : Maybe.of(fn(this._value))
    }
}

我们使用Maybe函子替代上面的Container,从而控制副作用

MayBe.of(null)
.map(x => x.toUpperCase()) 
// => MayBe { _value: null }

但是Maybe函子有个问题,如果一连串操作中有一个操作返回了null,我们无法知道从哪一步开始出的问题,因为返回null后,null会向下一直传递,直到最后一个函数调用结束,返回一个Maybe{_value: null}

let result = Maybe.of('hello world') 
.map(x => x.toUpperCase()) 
.map(x => null)
.map(x => x.split(' '))
console.log(result)
// Maybe { _value: null }

Either 函子

异常会让函数变的不纯,Either 函子可以用来做异常处理,Either函子定义了两种函子Left,Right,表示两种不同情况的处理结果,即正常情况和异常情况。

class Left {
    static of(value) {
        return new Left(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return this
    }
}

class Right {
    static of(value) {
        return new Right(value)
    }
    constructor(value) {
        this._value = value
    }
    map(fn) {
        return Right.of(fn(this._value))
    }
}

测试正常情况和异常情况

let sucResult = parseJSON('{ "name": "james" }') 
// 正常情况下才会执行
.map(x => {
    console.log(x.name)
})

console.log(sucResult)

let failResult = parseJSON([1, 2, 3]) 
// 异常情况下不会执行,所以函数内部可以只针对正常情况处理
.map(x => {
    console.log(x.name)
})

console.log(failResult)

输出

james
Right { _value: undefined }
Left {
  _value: Error: Unexpected token , in JSON at position 1
      at parseJSON (/Users/bingwu/Desktop/林肯/学习/node/JSLearning/learning.js:68:24)
      at Object.<anonymous> (/Users/bingwu/Desktop/林肯/学习/node/JSLearning/learning.js:80:18)
      at Module._compile (internal/modules/cjs/loader.js:1015:30)
      at Object.Module._extensions..js (internal/modules/cjs/loader.js:1035:10)
      at Module.load (internal/modules/cjs/loader.js:879:32)
      at Function.Module._load (internal/modules/cjs/loader.js:724:14)
      at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:60:12)
      at internal/main/run_main_module.js:17:47
}

IO 函子

IO 函子中的 _value 是一个函数,这里是把函数作为值来处理,延迟执行这个不纯的操作(惰性执行),将不纯的操作交给调用者来处理。

class IO {
    static of(value) {
        return new IO(() => value)
    }
    constructor(fn) {
        this._value = fn
    }
    map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}

为了说明IO函子的延时性,将其与Container进行对比:

// Container
console.log('c start')
let c = Container.of('james')
.map(x => {
    console.log('c1')
    return x.toUpperCase()
}).map(x => {
    console.log('c2')
    return x.charAt(0)
})
console.log('c result:' + c._value)

// IO
let io = IO.of("james")
.map(x => {
    console.log('io1')
    return x.toUpperCase()
}).map(x => {
    console.log('io2')
    return x.charAt(0)
})
console.log('io start')
console.log('io result: ' + io._value())

输出

c start
c1
c2
c result:J
io start
io1
io2
io result: J

可以发现,IO函子是将处理先进行保存,等到外部调用的时候才将值进行对应的操作

Monad

一个函子如果具有 join 和 of 两个方法并遵守一些定律就是一个 Monad,如实现join的IO函子:其实join就是对_value()的封装

class IO {
    static of(value) {
        return new IO(() => value)
    }
    constructor(fn) {
        this._value = fn
    }
    join() {
        return this._value()
    }
    map(fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}

调用

let monad = IO.of("james")
.map(x => {
    console.log('monad1')
    return x.toUpperCase()
}).map(x => {
    console.log('monad2')
    return x.charAt(0)
}).join()

下一篇:JS异步编程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容