logo

JavaScript 函数式编程 [WIP]

Fri Sep 08 2023 Posted last year

最近在看《JavaScript 函数式编程实践指南》这本小册,在这里简单整理一下里面的核心要点。

纯函数 #

what #

  1. 相同的输入总会返回相同的输出
  2. 执行过程中不会产生副作用(即函数除了内部计算外,还对它的执行上下文、执行宿主等外部环境造成了其它的影响)

why #

  1. 相同的输入总会返回相同的输出,减少了代码的不确定性,有利于代码测试。
  2. 执行过程中不产生副作用,所有的计算发生在函数内部,不会对外部资源产生影响,所以纯函数的并行计算总是安全的。
  3. 不依赖于特定上下文,使用起来更加灵活

可变数据 #

what #

Object 类型以外的值类型数据的都是不可变数据。而 Object 作为引用类型数据则是可变数据。

why not #

  1. 可变数据会使函数的行为变得难以预测,增加了不确定性
  2. 可变数据使函数复用的成本变高,一个可靠、受控的黑盒,应该总是将变化控制在盒子的内部,而不去改变盒子外面的任何东西

how to prevent #

  1. 通过深/浅拷贝得到副本,避免修改外部数据。函数的外部数据应该是只读的,内部数据则是可写的。
  2. 使用以 Immutable.js 为代表的实现持久化数据结构的库(快照 + 数据共享)。相关知识:字典树 (Trie)Immutable.js 源码
  3. 使用以 Immer.js 为代表的底层使用 ProxyAPI 的库。相关知识:Immer.js 源码

DRY & HOF #

what #

  1. DRY(Don't Repeat Yourself)将变与不变分离。
  2. HOF(High Order Function)以函数作为入参或者将函数作为返回值的函数。

why #

  1. 更简洁的代码
  2. 更好的可读性
  3. 代码可复用性更高
  4. 逻辑边界清晰,减少测试工作

pipe & compose #

what #

pipe 即从左往右调用 n 个函数,函数的入参是上一个函数的返回值。compse 就是反方向的 pipe。

pipe 的第一个入参函数应该只接收一个参数

// function pipe(...funcs) {
//     function callback(input,func){
//         return func(input)
//     }
    
//     return function(initialParam) {
//         return funcs.reduce(callback, initialParam)
//     }  
// }

const pipe  = (...funcs) => funcs.reduce(
  (f, g) => (...args) => g(f(...args))
);

// function compose(...funcs) {
//     // ...
//     return funcs.reduceRight(callback, initialParam)
// }

const compose  = (...funcs) => funcs.reduce(
  (f, g) => (...args) => f(g(...args))
);

柯里化 & 偏函数 #

what #

柯里化是指将接收 n 个参数的 n 元函数改造为 n 个相互嵌套的一元函数的过程。

偏函数是指通过固定函数的一部分参数来生成一个参数数量更少的函数的过程

即柯里化是把 n 元函数改造为 n 个一元函数。偏函数是把一个 n 元函数改造为 1 个 m(m<n) 元函数。

how #

通用柯里化函数:

  1. 获取传入的函数的参数数量
  2. 通过递归来自动分层嵌套函数
  3. 在嵌套的最后一层去调用回调函数并传入之前保存的所有入参
function curry(func, arity = func.length) {
    function getCurried(preArgs) {
        return function curried(nextArg) {
            // 当前已经拿到的参数
            const args = [...preArgs, nextArg]
            if (args.length >= arity) {
                // 得到足够的参数后执行传入的函数
                return func(...args)
            } else {
                // 否则继续去获取参数
               return getCurried(args) 
            }
        }
    }
    
    // 初始传入一个空数组,表示还没有拿到任何参数
    return getCurried([])
}

function add(a, b) {
  return a + b
}

function multiply(a, b, c) {
  return a*b*c
}

const curriedAdd = curry(add)
const curriedMultiply = curry(multiply)

const compute = pipe(
    curriedAdd(1),
    curriedMultiply(2)(3)
)
console.log(compute(3)) // 24
function multiply(x, y) {
  return x * y
}

function wrapFunc(func, fixedValue) {
  function wrappedFunc(input) {
    const newFunc = func(input, fixedValue)
    return newFunc
  }
  return wrappedFunc
}

const multiplyThree = wrapFunc(multiply, 3)

multiplyThree(2)

why #

  1. 更好地复用存量逻辑
  2. 减少重复传参