去往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

  记忆化的函数不仅可以用于提高计算效率,还可以用于缓存函数的副作用结果,以避免重复执行副作用操作。在某些场景下也非常有用。

posted @ 2023-05-31 16:11  艾路  阅读(7)  评论(0编辑  收藏  举报