去往js函数式编程(4)
日志记录
在调试代码时,通常需要添加一些日志信息来查看函数是否被调用,使用了哪些参数,返回了什么等等。
function someFunction(p1, p2, p3) {
console.log('enter', p1, p2, p3)
// do...
// and..
console.log('result', expression)
return expression
}
我们可以编写一个高阶函数,记录接收到参数,调用原始函数,并捕获其返回值,记录改值,将其返回给调用者。
const addLogging =
(fn) =>
(...args) => {
console.log(`进入${fn.name}:${args}`)
const valueToReturn = fn(...args)
console.log(`退出${fn.name}:${valueToReturn}`)
return valueToReturn
}
function subtract(a, b) {
b = changeSign(b)
return a + b
}
function changeSign(c) {
return -c
}
subtract = addLogging(subtract)
changeSign = addLogging(changeSign)
let x = subtract(7, 5)
// 进入subtract:7,5
// 进入changeSign:5
// 退出changeSign:-5
// 退出subtract:2
这对大多数函数都可以正常工作,但如果被包装的函数抛出异常,会出现一些问题。
function divide(a, b) {
if (b === 0) {
throw new Error('Division by zero')
}
return a / b
}
divide = addLogging(divide)
let result = divide(10, 2)
console.log(result)
result = divide(10, 0)
console.log(result)
// 进入 divide: 10, 2
// 退出 divide: 5
// 5
// 进入 divide: 10, 2
// 退出 divide: 5
// Unhandled exception: Error: Division by zero
问题在于我们的 addLogging 函数中,并没有正确处理异常情况。我们只需要添加一个 Try/catch 结构,就可以处理这个问题。
const addLogging =
(fn) =>
(...args) => {
console.log(`进入 ${fn.name}: ${args}`)
try {
const valueToReturn = fn(...args)
console.log(`退出 ${fn.name}: ${valueToReturn}`)
return valueToReturn
} catch (error) {
console.log(`抛出异常 ${fn.name}: ${error.message}`)
throw error
}
}
// 进入 divide: 10, 2
// 退出 divide: 5
// 5
// 进入 divide: 10, 0
// 抛出异常 divide: Division by zero
// Unhandled exception: Error: Division by zero
当然,为了获得更好的日志输出,你可以进行其他改进,比如添加日期和时间数据,改进参数的列举方式等等。我们在这基础上可以让他更纯净。我们可以将日志记录函数作为参数传递给包装函数,以便需要时进行更改。
const addLogging3 =
(fn, logger = console.log) =>
(...args) => {
logger(`进入 ${fn.name}: ${args}`)
try {
const valueToReturn = fn(...args)
logger(`退出 ${fn.name}: ${valueToReturn}`)
return valueToReturn
} catch (error) {
logger(`抛出异常 ${fn.name}: ${error.message}`)
throw error
}
}
如果我们什么都不做,日志记录包装器显然会产生与上面相同的结果。当然你假如使用 winston,就可以直接将他替换,结果也会相应地有所变化。
计时函数
我们希望能够知道函数调用需要多长时间,通常用于性能研究,和日志记录的方式一样,我们不希望修改原始函数,而是使用高阶函数来实现。如果你计划优化你的代码,不要优化,暂时不要优化,不要在没有测量的情况下进行优化。直到你认识到有必要这样做,并且不要随意进行优化。根据上面的例子,我们写一个 addTiming()函数。
const myPut = (text, name, tStart, tEnd) =>
console.log(`${name} - ${text} ${tEnd - tStart} ms`)
const myGet = () => performance.now()
const addTiming =
(fn, getTime = myGet, output = myPut) =>
(...args) => {
let tStart = getTime()
try {
const valueToReturn = fn(...args)
output('normal exit', fn.name, tStart, getTime())
return valueToReturn
} catch (error) {
output('exception thrown', fn.name, tStart, getTime())
throw error
}
}
记忆化函数
之前我们写过记忆化函数,拿斐波那契数列作为例子。我们把他也抽成高阶函数,自动完成记忆化的过程。我们假设记忆化函数只接收一个参数 x,而且它是一个原始值,可以直接作为缓存对象的键值使用。而且我们可以使用上面的 addTiming()函数来查看是否有用。
const fib = (n) => (n == 0 ? 0 : n == 1 ? 1 : fib(n - 2) + fib(n - 1))
const memoize = (fn) => {
let cache = {}
return (x) => (x in cache ? cache[x] : (cache[x] = fn(x)))
}
const testFib = (n) => fib(n)
addTiming(testFib)(45)
addTiming(testFib)(40)
addTiming(testFib)(30)
// testFib - normal exit 19422.19999998808 ms
// testFib - normal exit 1713 ms
// testFib - normal exit 14.5 m
fib = memoize(fib)
console.log(addTiming(fib)(45))
console.log(addTiming(fib)(40))
console.log(addTiming(fib)(10))
// - normal exit 0.09999999403953552 ms
// - normal exit 0 ms
// - normal exit 0 ms
每个人的计时结果跟 cpu,ram 有关。结果是合理的。这个是单参数函数,我们看下具有多个参数的函数。如果我们想要能够记忆任何函数,我们必须找到一种方法生成缓存键。为了做到这一代呢,我们必须找到一种将任何类型的参数转换为字符串的方法。我们不能直接使用非原始值作为缓存键。我们可以尝试类似 strX=String(x)的方法将值转换为字符串,但是会有问题。
var a = [1, 5, 3, 8, 7, 4, 6]
String(a) // "1,5,3,8,7,4,6"
var b = [
[1, 5],
[3, 8, 7, 4, 6]
]
String(b) // "1,5,3,8,7,4,6"
var c = [
[1, 5, 3],
[8, 7, 4, 6]
]
String(c) // "1,5,3,8,7,4,6"
这三种情况产生相同的结果。当不同的数组产生相同的键时,会有问题,如果我们接收对象作为参数,情况会变得更糟,因为任何对象的 String()表示都是"[Object Object]"。通过使用JSON.stringify()
,我们可以将任何类型的参数转换为可用且不同的字符串表示形式。我们可以根据这个思路来改进我们的记忆函数。
const memoize2 = (fn) => {
const cache = {}
return (...args) => {
const key = JSON.stringify(args)
return key in cache ? cache[key] : (cache[key] = fn(...args))
}
}
这个改进后的 memoize2 函数使用了剩余参数语法...args,允许接收任意数量的参数。它将参数转换为字符串形式,并将其用作缓存对象的键。如果缓存中存在该键,则直接返回缓存的值;否则,计算并将结果存入缓存对象。我们测试下:
const add = (a, b) => {
console.log('计算...')
return a + b
}
const memoizeAdd = memoize2(add)
console.log(memoizeAdd(2, 3)) //计算... 5
console.log(memoizeAdd(2, 3)) //5
console.log(memoizeAdd(4, 5)) //计算... 9
记忆化的函数不仅可以用于提高计算效率,还可以用于缓存函数的副作用结果,以避免重复执行副作用操作。在某些场景下也非常有用。