【问题标题】:JavaScript: Understanding closures and hoistingJavaScript:理解闭包和提升
【发布时间】:2017-08-02 13:12:17
【问题描述】:

我了解函数声明被提升到其范围的顶部。这允许我们在 JavaScript 中实际声明这些函数之前使用它们:

sayHello(); // It works!

function sayHello() {
  console.log('Hello');
}

我还理解闭包使函数能够保留对在同一范围内声明的变量的引用:

function outerFxn() {
  let num = 0;

  function innerFxn() {
    console.log(num);
    num++;
  }

  return innerFxn;
}

const logNum = outerFxn();
logNum(); // 0
logNum(); // 1
logNum(); // 2

到目前为止一切顺利。但是这里有一些奇怪的地方,我希望有人能准确地解释发生了什么......

场景 1:可理解的关闭

function zero(cb) {
  return setTimeout(cb, 0);
}

function test1() {
  let txt = 'this is a test message';

  function log() {
    console.log(txt);
  }

  zero(log);
}

在上面的示例中,log 函数保留了对创建它的作用域的引用,并保留了 txt 变量。然后,稍后在setTimeout 中执行时,它成功记录了txt 变量的值。伟大的。然后就是这个……

场景 2:发生了什么?

function zero(cb) {
  return setTimeout(cb, 0);
}

function test1() {
  function log() {
    console.log(txt);
  }

  let txt = 'this is a test message';

  zero(log);
}

我已经将 log 函数声明移到了作用域的顶部(无论如何它都会被提升到那里,对吗?),然后我在下面声明 txt 变量它。这一切仍然有效,我不知道为什么。当let 和const 没有被提升时,log 如何保留对txt 变量的引用?封闭范围是否被分析作为一个整体?我可以在这里逐步说明 JavaScript 引擎正在做什么。谢谢你!

【问题讨论】:

  • 你可以简单地使用这个替代 sn-p: function f() { console.log("a1: " + a); a = 123; console.log("a2: " + a) } 两个日志都打印 123

标签: javascript closures hoisting


【解决方案1】:

它是您离开test1 函数后范围的一部分。此时它是否与var、let 或const 一起使用并不重要。由于已经评估了整个主体,因此变量存在于范围内。

如果您在评估 let 声明之前尝试使用 log,则会收到错误消息。

编辑:从技术上讲,使用let 和const 声明的变量在作用域内,但它们被统一化,如果您尝试访问它们会导致错误。只有在您到达声明时,它们才被初始化并且您可以访问它们。因此它们始终在范围内,只是在评估声明之前不可用。

【讨论】:

  • 您的编辑在我看来是错误的。被提升的是vars(存在但未初始化),lets 在声明之前并不实际存在。
  • @georg,它们在声明之前确实存在。否则,它们可能会在更高的范围内被取代,但它们不会。它们是尚未初始化的引用,但它们确实存在。
  • 啊,我明白你的意思了。就像{ let p = 1; { p++; let p = 2 } },对吧?所以他们在声明之前有点“不使用”的标记。好点子!
【解决方案2】:

“是否对闭包范围进行了整体分析?” - 是的。闭包保留范围,因为它是你(词法上)离开它的那一刻。在您的示例中,当您到达test1 中的关闭} 时,txt 确实存在,因此它在范围内,log 访问它没有问题。

请注意上面的“词法”:绑定是在运行时之前完成的,此时唯一重要的是您的块结构。所以即使这样也行,尽管从“动态”的角度来看它不应该:

function test1() {
    function log() {
        console.log(txt);
    }

    zero(log);
    let txt = 'this is a test message';
}

【讨论】:

  • 我有一种预感,范围是作为一个整体进行分析的。 “......你词汇离开它的那一刻” - 那是我不明白的。谢谢!
【解决方案3】:

在场景2中你正在做:

  1. let txt = 'this is a test message' 这意味着txt 将成为test1() 范围的一部分。
  2. 同时您声明log() 可以访问 其父 test1() 的范围。

那么在运行时会发生什么? test1() 将被评估,因此log() 将有权访问test1() 的范围。这意味着txt 将可供log() 立即使用。

提示:调试它,设置一些断点,看看会发生什么。

编辑:您还可以考虑在log() 中,txt 没有定义,因此它的值应该是未定义的......对吗? console.log(txt) 工作输出 this is a test message 的事实是由于上面对范围的解释。在函数作用域的顶部声明变量总是一个好习惯,在作用域的底部声明函数,因为无论如何它们都会被首先评估。考虑到这种情况下的人为因素,最佳实践也可能意味着:让您/任何人通过阅读代码来了解代码的作用。

【讨论】:

    【解决方案4】:

    这是一个时间/执行顺序的事情。想一想

    function test1(){
        var context = { }; 
    
        function log(){
            if(context.hasOwnProperty("txt")){
                console.log(context.txt); 
            }else{
                throw new Error("there is no value 'txt' declared in this context");
            }
        }
    
        context.txt = 'this is a test message';
        log();
    }
    

    在您的代码中与未提升变量 txt 相同。当时,log 被执行,let txt 将在正确的函数上下文中声明。即使没有吊起也可以使用。函数log 不存储对变量本身的引用,而是对整个周围执行上下文的引用,并且此上下文存储变量。

    【讨论】:

      猜你喜欢
      • 2011-10-09
      • 1970-01-01
      • 2016-01-30
      • 1970-01-01
      • 1970-01-01
      • 2013-03-25
      • 2012-03-06
      • 1970-01-01
      相关资源
      最近更新 更多