如何学习 JavaScript 中的闭包并了解何时使用它们

2025-06-09

如何学习 JavaScript 中的闭包并了解何时使用它们

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

如果你像我一样,听到过诸如词法环境闭包执行上下文之类的概念,你会觉得“是的,我听说过”,就是想不起来它们是什么,但我可能正在用它们。你知道吗,你是对的。你很可能正在用它们,但谁又能记住这些术语呢?

我的意思是,我们很可能只有在准备 JavaScript 面试的时候才需要知道这些术语的名称。我不是说不要学习这些概念,而是说只要你知道它们的工作原理,即使你给它们起了个不同的名字,世界也不会崩溃:

熊爪

我们知道在面试时我们需要了解这些术语,其余时间我们只需要知道编码时事情是如何运作的,我们也确实这样做了。

让我们深入探讨一下,为什么我们能理解甚至运用这些术语,却不知道它们叫什么?是命名不当吗?或许,就我而言,我意识到自己是一个视觉学习者,我需要借助图像来记忆,否则记忆力就无法持久。

欢迎来到我大脑的疯狂之旅 - 让我们谈谈闭包:) - 所有人都登上疯狂的火车;)疯狂的火车

闭包

什么是闭包?闭包是与其词法环境捆绑在一起的函数。

谢谢教授,但我几乎听不懂一个字。

好的,让我们看一些代码:

function outer() {
  // lexical environment
  let a = 1;
  return function inner(b) {
    return a + b
  }
}

上面你看到的是一个函数outer() 包裹着另一个函数inner。它不仅包裹着函数 inner(),还包裹着变量a

这有什么了不起的?

即使在函数outer()停止执行之后,函数inner()仍然可以访问其词法环境,在本例中为变量a

词汇环境,听起来像词典,又大又重的书。给我看看。

好的,想象一下我们这样调用代码:

const fn = outer();
fn(5) // 6

上面它记得 a有价值1

好的,所以它就像是a一个私有变量,但是在函数中?

是的,确实如此。

我知道该如何记住这一点:)

是的?

奶牛

牛?!

是的,奶牛在一个封闭的环境中,外部函数作为封闭体,奶牛作为内部函数和私有变量,如下所示:

围栏内的奶牛

噢,慢慢地走开了。

我们可以用它们来做什么

好的,我们对闭包有了一些介绍,但让我们说明一下我们可以用它们来做什么:

  • 通过创建私有变量,我们可以在外部函数执行完毕后很长时间内创建一个词法环境,这使得我们可以将词法环境视为类中的私有变量。这样我们就可以编写如下代码:
   function useState(initialValue) {
     let a = initialValue;
     return [ () => a, (b) => a = b];
   }

   const [health, setHealth] = useState(10);
   console.log('health', health()) // 10
   setHealth(2);
   console.log('health', health()) // 2

a上面我们看到了如何返回一个数组,该数组公开了从词法环境返回和设置变量的方法

  • 部分应用,其思想是接受一个参数但不完全应用它。我们在第一个例子中已经展示了这一点,但让我们展示一个更通用的方法partial()
  const multiply = (a, b) => a * b;

  function partial(fn, ...outer) {
    return function(...inner)  {
      return fn.apply(this, outer.concat(inner))
    }
  }

  const multiply3 = partial(multiply, 3);
  console.log(multiply3(7)) // 21

上面的代码收集了第一个函数的所有参数outer,然后返回内部函数。接下来,你可以调用返回值,因为它是一个函数,如下所示:

  console.log(multiply3(7)) // 21

好的,我想我明白了它的工作原理。那么应用程序呢?我什么时候真正使用它呢?

嗯,它有点像学术构造,但它肯定在库和框架中使用。

就是这样?

我的意思是,您可以使用它使功能更加专业化。

仅举一个例子?

当然,这里有一个:

  const baseUrl = 'http://localhost:3000';

  function partial(fn, ...args) {
      return (...rest) => {
        return fn.apply(this, args.concat(rest))
      }
  }

  const getEndpoint = (baseUrl, resource, id) => {
      return `${baseUrl}/${resource}/${id ? id: ''}`;
  }

  const withBase = partial(getEndpoint, baseUrl);
  const productsEndpoint = withBase('products')
  const productsDetailEndpoint = withBase('products', 1)

  console.log('products', productsEndpoint);
  console.log('products detail', productsDetailEndpoint);

上面是一个相当常见的场景,构建一个 URL 端点。我们创建了一个更特殊的版本withBase,部分应用了baseUrl。然后我们继续添加具体的资源,如下所示:

   const productsEndpoint = withBase('products')
   const productsDetailEndpoint = withBase('products', 1)

它不是必须的但它很有用,可以减少代码的重复性。它是一种模式。

  • 隔离部分代码/通过 JavaScript 面试,为此,我们先来展示一个在 JS 面试中很常见的问题。我连续三次面试都被问到同样的问题。如果你用 Google 搜索一下,也能找到这个问题。你猜怎么着,JavaScript 面试流程有问题。

什么意思?坏了?

没人会在意你是否有多年的经验,或者了解很多框架。面试官通常会花 5 分钟在谷歌上搜索 JavaScript 问题来问你。

听起来他们问的是 JavaScript 语言及其核心概念。不是很好吗?

是的,这部分不错,但是 JavaScript 本身就有很多怪异之处,所以 Crockford 写了一本名为《JavaScript 的优秀之处》的书,这本书很薄,是有原因的。这本书确实有优点,但也有很多怪异之处。

你要告诉我一个面试问题吗?

好的,这是代码,你能猜出答案吗?

   for (var i = 0; i < 10; i++) {
    setTimeout(() => {
      return console.log(`Value of ${i}`);
    }, 1000)
   }  

1,2,3,4,5,6,7,8,9,10

沒有受傷。

好冷啊,能告诉我为什么吗?

setTimeout是异步的,并在几毫秒后调用1000。for 循环会立即执行,因此在setTimeout调用时i,参数将达到最大值10。因此它会打印10,10次。但我们可以修复它,使其以升序打印。

如何?

通过创建范围,在代码中实现隔离,如下所示:

   for (var i = 0; i < 10; i++) {
     ((j) => setTimeout(() => {
      return console.log(`Value of ${j}`);
    }, 1000))(i)
   }

上面创建了一个立即调用函数表达式,IIFE(看起来确实有点不确定,对吧;)?)。它实现了隔离,其中的每个值都绑定到特定的函数定义和执行i

除了上述解决方案之外,还有一种替代方案,那就是使用let。该let关键字会创建一个作用域代码块。因此,代码应该如下所示:

   for (let i = 0; i < 10; i++) {
    setTimeout(() => {
      return console.log(`Value of ${i}`);
    }, 1000)
   }  

感谢 Quozzo 指出这一点。

概括

好吧,所以整个封闭事件都与奶牛、栅栏和隐私有关

还有 JavaScript ;)

链接:https://dev.to/itnext/how-you-can-learn-closures-in-javascript-and-understand-when-to-use-them-2lk5
PREV
如何使用 React Testing Library 来测试组件表面
NEXT
如何在 NodeJS 中处理海量数据 dev.to 文章 - 如何在 NodeJS 中处理海量数据