【问题标题】:Trying to understand scope and closures试图理解范围和闭包
【发布时间】:2024-01-02 04:05:01
【问题描述】:

我已经阅读了一些教程来了解 JavaScript 中的作用域和闭包,并遇到了下面的代码。

我理解输出为 5,5,5,5,5 的第一个块,因为该函数在 for 循环完成后执行。但是我不完全理解为什么第二个块起作用......我是否认为在每次迭代中都会调用一个新函数,所以内存中同时运行了 5 个函数?我想要一个简单易懂的解释 - 我是学习 JavaScript 的新手。

for (var i = 0; i < 5; i++) {
  setTimeout(function () {
    console.log('index: ' + i);
  }, 1000);
}


for (var i = 0; i < 5; i++) {
  (function logIndex(index) {
    setTimeout(function () {
      console.log('index: ' + index);
    }, 1000);
  })(i)
}

【问题讨论】:

  • Javascript 是按值调用原语的。将第二个示例替换为for (var i = { i: 0 }; i.i &lt; 5; i.i++) {console.log('index: ' + index.i);,您将再次遇到与第一个示例相同的问题。
  • 我也刚刚在另一个教程中遇到过这段代码,我很困惑,因为它看起来与这里的第一个示例非常相似,只是它以 1 秒的间隔打印出 10,我不明白为什么。它与此相关,因此我将其添加为注释而不是单独的线程: for (var i = 0; i
  • 请注意,虽然上面链接的问题与此不完全相同,但有多种迹象表明它可以解决大部分问题 - 例如。 “为什么 let 会起作用而 var 不起作用”,范围界定规则,进一步的例子和关于这个问题的大量材料。这两个代码 sn-ps 也经常出现在答案中并进行了解释。
  • @ASDFGerte 大部分问题不是为什么 let 可以工作而 var 不能工作,这是对所发表评论的回复。我相信你也可以说这里有很多关于任何问题的材料。

标签: javascript function scope closures scoping


【解决方案1】:

是的,你是对的 5 个函数将执行,不需要logIndex,你可以使用anonymous function 进行此类工作。

(funtion(index){}) => function 定义

(funtion(index){})(i) => 通过 i 调用 function

for (var i = 0; i < 5; i++) {
  (function (index) {
    setTimeout(function () {
      console.log('index: ' + index);
    }, 1000);
  })(i)
}

【讨论】:

  • logIndex 在我正在阅读的教程中提供。好的,谢谢,我希望在答案中提供更多的演练。
  • 我更新了一些解释,现在让你感到困惑的是什么?
【解决方案2】:

您的示例 2 ,即:

for (var i = 0; i < 5; i++) {
  (function logIndex(index) {
    setTimeout(function () {
      console.log('index: ' + index);
    }, 1000);
  })(i)
}

工作正常,因为在示例中,您使用闭包为每个超时函数安排了一个不同的“i”副本。

你甚至可以使用 let 来实现,试试下面的方法:

for (let i = 0; i < 5; i++) {
    setTimeout(function () {
      console.log('index: ' + i);
    }, 1000);
}

这是有效的,因为在一个基于 let 的索引的循环中,循环中的每次迭代都会有一个新值 i,其中每个值都在循环内,因此您的代码将按预期工作。

更多解释可以参考:Reference

【讨论】:

  • 谢谢 - 我不明白为什么 let 会起作用而 var 不起作用 - 他们不是都在做同样的事情,最多迭代 5 次吗?
  • let 可以正常工作,因为 let 变量与块作用域相关联,而 var 变量具有函数作用域。因此,在您的情况下, var i 绑定到最接近的功能范围,即窗口。在哪里让我每次都会有一个新值,它将在循环内进行。
  • 我现在明白了,也许一个演练的例子最初会有所帮助。
  • @jobe 请参考答案中提到的参考链接,它有很多例子和一步一步的解释。我希望它有所帮助。
【解决方案3】:

由于您在下面的 cmets,重构了我的答案。

在我们开始之前,我们需要解决几个术语:

  1. 执行上下文 - 简单来说,这是一个函数在其中执行的“环境”。例如,当我们的应用程序启动时,我们在“全局”执行上下文中运行,当我们调用一个函数时,我们会创建一个新的执行上下文(嵌套在全局中)。
    每个执行上下文都有一个变量环境(范围),当然还有函数体(它是“命令”)。

  2. 调用堆栈 - 跟踪我们在哪个执行上下文,以及每个执行上下文有哪些变量可供我们使用,当函数返回它从调用堆栈中弹出,并且它的环境被标记为垃圾收集(释放内存),除了我们稍后会发现的一个异常。

  3. Web 浏览器 API 和事件循环 - JavaScript 是单线程的(为了简单起见,我们称其为线程),但有时我们需要处理异步操作,例如点击事件、xhr 和计时器。
    浏览器通过其 API 公开它们,addEventListenerXHR/fetchsetTimeout 等...
    这里很酷的是它将在与 javascript 线程不同的线程上运行它。但是浏览器如何在主线程上运行我们的代码呢?通过我们提供给它的回调(就像您对 setTimeout 所做的那样)。
    好的,它什么时候会运行我们的代码?我们想要一种可预测的方式来运行我们的代码。
    进入事件循环和回调队列,浏览器将每个回调推送到这个队列(promise 到另一个具有更高优先级的队列)并且事件循环正在监视调用堆栈,当调用堆栈时是空的并且没有更多的代码可以在全局中运行事件循环将抓取下一个回调并将其推送到调用堆栈。

  4. 闭包 - 简单来说,就是当一个函数访问它的词法(静态)范围时,即使它运行在它之外。以后会更清楚的。


在示例 #1 - 我们在 全局执行上下文 上运行一个循环,创建一个变量 i 并在每次迭代时将其更改为一个新值,而向浏览器传递 5 个回调(通过 setTimeout API)。
事件循环不能将这些回调推回调用堆栈,因为它还不是空的。当循环结束时,调用堆栈为空,事件循环将我们的回调推送给它,每个回调访问i并打印最新值5(关闭,我们访问i环境之后的方式它应该被摧毁)。原因是所有回调都是在相同的执行上下文中创建的,因此它们引用相同的i

在示例 #2 - 我们在 全局执行上下文 上运行一个循环,在每次迭代时创建一个新函数 (IIFE),从而创建一个新的执行-语境。这将在这个执行上下文中创建i 的副本,而不是像以前那样在全局上下文中。在这个执行上下文中,我们通过setTimeout 发送回调,就像在事件循环等待循环完成之前一样,因此调用堆栈将为空并将下一个回调推送到堆栈。但是现在当回调运行时,它会访问创建它的执行上下文并打印全局上下文从未更改过的i

所以基本上我们有 5 个执行上下文(没有全局),每个都有自己的i

希望现在更清楚了。

我真的推荐观看 this 关于事件循环的视频。

【讨论】:

  • 我要求一个简单易懂的解释,你提到了事件循环、调用堆栈和我还没有涉及到的东西。
  • 抱歉,您提到您了解第一个块,所以我假设您熟悉这些术语。我的意思是,如果你真的理解为什么第一个代码在 for 循环结束后运行,那么你应该了解事件循环。如果您愿意,我不介意尝试解释它
  • 没问题,我了解正在发生的某些部分,但没有区别。到目前为止,没有人回答是否有 5 个函数在内存中同时运行,一个简单的解释会对我有所帮助。如果你认为你可以用简单的方式解释事件循环,那么我愿意理解。
  • setTimeout however is not running on javascript's thread 只是一个小问题,这里不涉及任何线程。甚至不是工作线程,定时器都是基于中断的。
【解决方案4】:
for (var i = 0; i < 5; i++) {
    (function logIndex(index) {
        setTimeout(function () { console.log(index); }, 1000); // 0 1 2 3 4
    })(i)
}

您的代码将在内部创建 一个闭包,并与 每个 迭代的 当前 索引值绑定。因此,您完全是在创建具有不同索引值的 5 闭包。

一旦每个闭包内的 setTimeout 结束,它就会打印它们在本地可见的索引值。

【讨论】: