JavaScript函数式编程中的备忘录模式与纯函数边界
在函数式编程中,纯函数因其可预测性而备受推崇——相同的输入永远产生相同的输出。这一特性天然支持了"备忘录"(Memoization)技术,即缓存函数计算结果,避免重复执行昂贵运算。应用此技术时,我们只在首次计算表达式时执行,并将结果存入缓存,后续调用直接返回缓存值。
斐波那契数列是演示备忘录的经典案例,它的递归定义天然具有大量重复计算。原始实现如下:
const fibonacci = (n) => {
if (n === 0) return 0
if (n === 1) return 1
return fibonacci(n - 2) + fibonacci(n - 1)
}
console.log(fibonacci(10)) // 55
随着 n 增大,计算时间呈指数增长。引入备忘录后,我们用一个缓冲区存储已知结果:
const memoFib = (() => {
const cache = {}
const fib = (n) => {
if (n in cache) return cache[n]
if (n === 0) cache[n] = 0
else if (n === 1) cache[n] = 1
else cache[n] = fib(n - 2) + fib(n - 1)
return cache[n]
}
return fib
})()
console.log(memoFib(10)) // 55,速度大幅提升
注意,这里使用 IIFE 和闭包隐藏了缓存,避免全局变量污染。但需谨慎:若缓存数据量过大,可能导致内存耗尽。因此,备忘录并非适用于所有纯函数——仅当函数调用频繁、计算开销大且输入范围有限时值得采用。
纯函数还有一个显著优势:所有依赖通过参数显式传递,没有隐藏的外部状态。阅读源码时,你掌控了函数行为的全部信息;使用函数时,无需担心意外副作用。但过度追求"无副作用"也可能不切实际。实际开发中,我们应努力减少副作用,当不可避免时,需思考如何限制其影响范围。
处理全局状态(读取或修改)的经典做法是:将全局状态作为参数传给函数;若函数需要更新状态,不直接修改,而是返回新状态;由调用方负责用新状态更新全局变量。这正是 Redux reducer 的核心模式:(previousState, action) => newState。reducer 必须保持 previousState 不变。
当一个函数因调用非纯函数而变得不纯时,可通过依赖注入来解耦。例如,一个生成随机文件名的函数内部使用了随机字母生成器:
const getRandomFileName = (extension = '') => {
const NAME_LENGTH = 12
const parts = []
for (let i = 0; i < NAME_LENGTH; i++) {
parts.push(getRandomLetter())
}
return parts.join('') + extension
}
改进版本将随机字母生成函数作为参数注入:
const getRandomFileName2 = (extension = '', randomLetterFn) => {
const NAME_LENGTH = 12
const parts = []
for (let i = 0; i < NAME_LENGTH; i++) {
parts.push(randomLetterFn())
}
return parts.join('') + extension
}
相应的随机字母生成函数也可注入随机数生成器:
const getRandomLetter = (random = Math.random) => {
const min = 'A'.charCodeAt()
const max = 'Z'.charCodeAt()
return String.fromCharCode(Math.floor(random() * (1 + max - min)) + min)
}
即使我们将函数写得尽可能纯,JavaScript 的动态特性仍可能让纯函数"变质"。考虑一个看似纯函数的求和函数:
const sum3 = (x, y, z) => x + y + z
初看它只依赖参数,不修改变量,不执行 I/O。但若传入一个自定义对象,其 valueOf 方法返回随机数:
const obj = { valueOf: Math.random }
console.log(sum3(obj, 1, 2)) // 每次输出不同
因此,JavaScript 中函数的纯度不仅取决于函数体本身,还取决于运行时参数的行为。这提醒我们,在边界情况下,纯函数的假设可能被打破,需要格外审慎。