2.2.1 基于JavaScript高阶函数的编码优化

1. 惰性求值

在介绍Lambda表达式和不可变数据结构时,我们都提到了程序执行的次序。

常规程序在运行时一般遵循应用序或正则序。应用序应用于大多数语言,是指先求值参数而后应用;正则序则是完全展开而后归约。这两种执行顺序在不同场景下有各自的优势,虽然大多数语言使用应用序,但我们仍然希望特定场景的代码能具备正则序运算、惰性求值的能力。

惰性求值(Lazy Evaluation/Call-By-Need)支持在程序运行优化时,消解掉一部分不必执行的代码。在控制代码结构时,也可以少运行一些不能触及的方法参数,并且允许代码中出现无穷计算的数据结构,比如自然数队列。

在依赖数据执行的次序或副作用的场景下,应用序的场景是必不可少的。

JavaScript中我们可以使用thunk函数实现惰性求值,也就是把要运行的代码放在一个未执行的函数中,手动控制函数的调用事件。

在前端编码中,我们可以通过两种方式模拟代码执行次序:一种是直接调用已有的函数(实时求值的应用序);另一种是使用已有数据生成thunk函数但并不立即调用(蓄而不发的正则序)。遇到更复杂的应用场景时,我们可以包装人为控制的生成器(generator),或者其他一些可控制迭代的数据类型,以此来实现更高阶的惰性求值。

代码清单2-4展示了正则序运算和JavaScript中thunk的实现。

代码清单2-4 正则序运算和JavaScript中thunk的实现


// 正则序
(sum-of-squares( + 5 1) (* 5 2))
-> (+ (squares ( + 5 1)) (squares (* 5 2)))
-> (+ (* (+ 5 1)(+ 5 1)) (* (* 5 2)(* 5 2)))
---
-> (+ (* 6 6) (* 10 10))
-> (+ 36 100)
-> 136

// Thunk
const x = 1, y = 2, z = 5;
const plusThunk = function () {
  return x + z;
};
const multiplyThunk = function () {
  return y * z;
};
const squares = x => x * x
function sumOfSquares(tempFuncA, tempFuncB){
  return squares(tempFuncA()) +squares(tempFuncB());
};

sumOfSquares(plusThunk, multiplyThunk);  // 136

2. 函数组合和无参数风格

通过Lodash4中的_.flow和_.flowRight方法,我们可以像拼自来水管道的游戏一样把多个函数拼接起来,这很好地解决了函数的组合问题。

当多个函数串行调用时,funcA、funcB、funcC可以通过'_.flow([funcA, funcB, funcC])'的形式调用,而不是'funcA(funcB(funcC()))'。第一种方法除了更直观以外,也方便我们插入一些对方法再加工的切面编程能力。

这种无参数风格(Pointfree)更关注对方法进行抽离和组合,实际使用的时候也要结合代码可读性和程序的拆分程度进行编码设计。我们在第5章讨论代码形式时会进一步剖析这个能力。

3. 柯里化和部分施用函数、偏函数

最后我们引入3个在运用函数式思维时会接触的概念。这3个概念是类似的,即把函数的部分参数固定后,产生新的函数的过程;柯里化(Currying)则更进一步,它将多个参数的函数直接打散,对参数赋值后,都可以产生新的部分施用函数(Partial Application)。偏函数(Partial Function,比如Python中的偏函数)在实现类似功能时,顺序有些不同,思想和部分施用函数类似。

代码清单2-5所示是本节3个概念的示例。

代码清单2-5 JavaScript中柯里化和部分施用函数、偏函数示例


// 参数复用
const obj = { name: 'test' }
const foo = function (prefix, suffix) {
  console.log(prefix + this.name + suffix)
}.bind(obj, 'currying-')

foo('-function'); // currying-test-function

// 柯里化/延迟计算
var add4args = (x,y,z,t) => x + y + z + t
var add = _.curry(add4args)
add4args(2, 3, 4, 5)
add(2)(3)(4)(5)
add(2)(3, 4)(5)

// 部分施用函数/偏函数
function partial(func, ...argsBound) {
  return function(...args) {
    return func.call(this, ...argsBound, ...args)
  }
}

function printLog(time, log) {
  alert('[${time}] ${this.firstName}: ${log}!')
}

let _now = new Date()
let printNow = partial(printLog, _now.getHours() + ':' + _now.getMinutes())
printNow('get shopid error')

柯里化对函数方法的粒度进行了最彻底的拆解。就像一些重构者希望每个模块只有少数几行可执行代码一样,柯里化帮助我们在执行编码时把函数方法拆解到最小。

在JavaScript中,提前使用部分施用函数可以减少参数数量,以方便对函数进行组合和拆分,实现参数的复用;也可以包装thunk和帮助延迟计算、动态生成函数(函数工厂);我们还可以方便地使用模板和隐藏一些接口调用的参数。