利用闭包实现柯里化与函数记忆(Memoization)

好的,各位观众老爷们,欢迎来到“闭包奇妙夜”!今晚,咱们要聊聊两位武林高手:柯里化(Currying)和函数记忆(Memoization)。他们都是闭包这门内功心法的杰出代表,能让你的代码变得更优雅、更高效,还能让你在面试中秀翻全场!😎

准备好了吗?让我们一起揭开他们的神秘面纱!

第一幕:闭包——内功心法的根基

在开始之前,咱们先来复习一下闭包这个老朋友。你可以把它想象成一个“记忆盒子”,一个函数可以记住并访问它被创建时所处的环境(也就是词法作用域)。

function outerFunction(x) {
  function innerFunction(y) {
    return x + y;
  }
  return innerFunction;
}

let add5 = outerFunction(5); // add5 现在是一个闭包
console.log(add5(3)); // 输出 8 (add5 记住了 x 的值为 5)

在这个例子中,innerFunction 就是一个闭包。它记住了 outerFunction 被调用时 x 的值。即使 outerFunction 已经执行完毕,innerFunction 仍然可以访问并使用 x

你可以把闭包想象成一个武林高手,他/她练就了一门特殊的内功,能够记住过去发生的事情,并将其运用到未来的战斗中。这门内功,就是闭包的核心价值!

第二幕:柯里化——化繁为简的剑法

柯里化,英文名 Currying,听起来是不是有点像咖喱?🍛 但它可不是一道菜,而是一种函数转换的技术。它的核心思想是:把一个接受多个参数的函数,转换成一系列接受单个参数的函数。

你可以把柯里化想象成一种“化繁为简”的剑法。原本需要一招才能解决的敌人,现在可以拆分成几个小招来逐个击破。

柯里化的原理

柯里化的基本原理就是利用闭包来保存每一次传入的参数,直到收集到足够的参数后,再执行最终的计算。

function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn(...args); // 参数足够,执行原函数
    } else {
      return function(...nextArgs) {
        return curried(...args, ...nextArgs); // 参数不足,返回新的函数
      };
    }
  };
}

function add(x, y, z) {
  return x + y + z;
}

let curriedAdd = curry(add);

console.log(curriedAdd(1)(2)(3)); // 输出 6
console.log(curriedAdd(1, 2)(3)); // 输出 6
console.log(curriedAdd(1)(2, 3)); // 输出 6
console.log(curriedAdd(1, 2, 3));   // 输出 6

在这个例子中,curry 函数接受一个函数 fn 作为参数,并返回一个新的函数 curriedcurried 函数会检查传入的参数数量是否达到 fn 所需的参数数量。如果达到,就执行 fn;否则,就返回一个新的函数,该函数会把之前的参数和新的参数合并起来,再次调用 curried

柯里化的应用场景

  • 参数复用: 当你需要在多个地方使用同一个函数,并且其中一些参数是固定的,可以使用柯里化来预先设置这些参数。
  • 延迟执行: 柯里化可以将函数的执行延迟到所有参数都准备好之后。
  • 代码组合: 柯里化可以方便地将多个函数组合成一个新的函数。
  • 函数式编程: 柯里化是函数式编程中常用的技术,可以使代码更加简洁和易于维护。

举个栗子🌰:格式化字符串

假设我们有一个函数,用于格式化字符串:

function formatString(template, data) {
  return template.replace(/{(w+)}/g, (match, key) => data[key] || '');
}

let template = "Hello, {name}! You are {age} years old.";
let data = { name: "Alice", age: 30 };

console.log(formatString(template, data)); // 输出 "Hello, Alice! You are 30 years old."

现在,如果我们想在多个地方使用这个函数,但是每次都使用相同的模板,可以使用柯里化来预先设置模板:

let formatWithTemplate = curry(formatString)(template);

console.log(formatWithTemplate({ name: "Bob", age: 25 })); // 输出 "Hello, Bob! You are 25 years old."
console.log(formatWithTemplate({ name: "Charlie", age: 35 })); // 输出 "Hello, Charlie! You are 35 years old."

这样,我们就只需要传入数据即可,而不需要每次都传入模板。

第三幕:函数记忆——空间换时间的妙招

函数记忆(Memoization),顾名思义,就是让函数记住之前计算过的结果,避免重复计算。这是一种典型的“空间换时间”的策略。

你可以把函数记忆想象成一个学霸的笔记本。学霸把做过的题和答案都记录下来,下次遇到同样的题,直接查笔记本,而不需要重新计算。

函数记忆的原理

函数记忆的基本原理就是使用一个缓存对象(通常是一个 Map),来存储函数的输入和输出。当函数被调用时,首先检查缓存中是否已经存在该输入的输出。如果存在,直接返回缓存中的结果;否则,执行函数,并将结果存储到缓存中。

function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args); // 将参数转换为字符串作为 key

    if (cache.has(key)) {
      return cache.get(key); // 从缓存中获取结果
    } else {
      const result = fn(...args);
      cache.set(key, result); // 将结果存储到缓存中
      return result;
    }
  };
}

function fibonacci(n) {
  if (n <= 1) {
    return n;
  }
  return fibonacci(n - 1) + fibonacci(n - 2);
}

let memoizedFibonacci = memoize(fibonacci);

console.time("with memoization");
console.log(memoizedFibonacci(40)); // 输出 102334155
console.timeEnd("with memoization");

console.time("without memoization");
console.log(fibonacci(40)); // 输出 102334155
console.timeEnd("without memoization");

在这个例子中,memoize 函数接受一个函数 fn 作为参数,并返回一个新的函数。这个新的函数会检查缓存中是否已经存在该输入的输出。如果存在,直接返回缓存中的结果;否则,执行函数,并将结果存储到缓存中。

我们可以看到,使用函数记忆后,计算 fibonacci(40) 的时间大大缩短。

函数记忆的应用场景

  • 计算密集型函数: 对于需要大量计算的函数,可以使用函数记忆来避免重复计算。
  • 纯函数: 函数记忆只适用于纯函数,即对于相同的输入,总是返回相同的输出,并且没有副作用。
  • 递归函数: 函数记忆可以有效地优化递归函数的性能。

举个栗子🌰:计算阶乘

function factorial(n) {
  if (n === 0) {
    return 1;
  }
  return n * factorial(n - 1);
}

let memoizedFactorial = memoize(factorial);

console.log(memoizedFactorial(5)); // 输出 120
console.log(memoizedFactorial(5)); // 输出 120 (直接从缓存中获取)

第四幕:柯里化与函数记忆的结合

既然柯里化和函数记忆都是闭包的优秀应用,那么它们可以结合使用吗?答案是肯定的!🎉

我们可以使用柯里化来预先设置一些参数,然后使用函数记忆来缓存计算结果。

举个栗子🌰:带缓存的格式化字符串

function memoizeCurried(fn) {
  const cache = new Map();

  return function curried(...args) {
    const key = JSON.stringify(args);

    if (cache.has(key)) {
      return cache.get(key);
    } else {
      const result = fn(...args);
      cache.set(key, result);
      return result;
    }
  };
}

let memoizedCurriedFormatString = memoizeCurried(curry(formatString));

let formatWithTemplateAndCache = memoizedCurriedFormatString(template);

console.log(formatWithTemplateAndCache({ name: "Bob", age: 25 })); // 输出 "Hello, Bob! You are 25 years old."
console.log(formatWithTemplateAndCache({ name: "Bob", age: 25 })); // 输出 "Hello, Bob! You are 25 years old." (直接从缓存中获取)
console.log(formatWithTemplateAndCache({ name: "Charlie", age: 35 })); // 输出 "Hello, Charlie! You are 35 years old."

在这个例子中,我们首先使用 curry 函数将 formatString 函数柯里化,然后使用 memoizeCurried 函数将柯里化后的函数进行记忆。这样,我们就可以预先设置模板,并且缓存计算结果,从而提高性能。

第五幕:总结与展望

今晚,我们一起学习了闭包的两位杰出代表:柯里化和函数记忆。

  • 柯里化: 是一种化繁为简的剑法,可以将一个接受多个参数的函数,转换成一系列接受单个参数的函数。
  • 函数记忆: 是一种空间换时间的妙招,可以让函数记住之前计算过的结果,避免重复计算。

它们都是闭包的优秀应用,可以使你的代码更加优雅、更高效。

特性 柯里化 (Currying) 函数记忆 (Memoization)
核心思想 将一个多参数函数转化为一系列单参数函数 缓存函数执行结果,避免重复计算
应用场景 参数复用、延迟执行、代码组合、函数式编程 计算密集型函数、纯函数、递归函数
优点 提高代码的可读性和可维护性,方便进行函数组合,可以预先设置一些参数 提高函数的执行效率,避免重复计算
缺点 可能会增加代码的复杂性,需要额外的内存来存储中间结果 需要额外的内存来存储缓存结果,只适用于纯函数
与闭包关系 柯里化函数返回的是一个闭包,闭包保存了已经传入的参数,并在后续调用中继续使用这些参数 函数记忆通过闭包来维护一个缓存对象,用于存储函数的输入和输出
示例代码 javascript function curry(fn) { return function curried(...args) { if (args.length >= fn.length) { return fn(...args); } else { return function(...nextArgs) { return curried(...args, ...nextArgs); }; } }; } | javascript function memoize(fn) { const cache = new Map(); return function(...args) { const key = JSON.stringify(args); if (cache.has(key)) { return cache.get(key); } else { const result = fn(...args); cache.set(key, result); return result; } }; }

希望今晚的“闭包奇妙夜”能让你对柯里化和函数记忆有更深入的了解。在实际开发中,灵活运用它们,你的代码将会更加精彩!✨

感谢大家的观看,我们下期再见!👋

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注