首页 > 解决方案 > 试图理解范围和闭包

问题描述

我已经通过一些教程来了解 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)
}

标签: javascriptfunctionscopeclosuresscoping

解决方案


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

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

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

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

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

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


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

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

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

希望现在更清楚了。

我真的建议观看有关事件循环的视频


推荐阅读