【问题标题】:Why does Chrome debugger think closed local variable is undefined?为什么 Chrome 调试器认为封闭的局部变量是未定义的?
【发布时间】:2015-02-07 23:04:44
【问题描述】:

使用此代码:

function baz() {
  var x = "foo";

  function bar() {
    debugger;
  };
  bar();
}
baz();

我得到了这个意想不到的结果:

当我更改代码时:

function baz() {
  var x = "foo";

  function bar() {
    x;
    debugger;
  };
  bar();
}

我得到了预期的结果:

另外,如果在内部函数中有任何对eval 的调用,我可以随意访问我的变量(无论我传递给eval 的内容是什么)。

同时,Firefox 开发工具在这两种情况下都提供了预期的行为。

调试器不如 Firefox 方便的 Chrome 是怎么回事?我已经观察到这种行为一段时间了,直到并包括版本 41.0.2272.43 beta(64 位)。

是不是 Chrome 的 javascript 引擎会尽可能“扁平化”这些功能?

有趣的是,如果我在内部函数中添加 引用的第二个变量,x 变量仍然未定义。

我知道在使用交互式调试器时经常会出现范围和变量定义的怪癖,但在我看来,根据语言规范,应该有一个“最佳”解决方案来解决这些怪癖。所以我很好奇这是否是由于 Chrome 比 Firefox 优化得更远。还有这些优化是否可以在开发过程中轻松禁用(也许应该在开发工具打开时禁用它们?)。

另外,我可以使用断点以及debugger 语句来重现这一点。

【问题讨论】:

  • 也许它正在为您排除未使用的变量...
  • markle976 似乎在说 debugger; 行实际上并没有从 bar 内部调用。因此,在调试器中暂停时查看堆栈跟踪:堆栈跟踪中是否提到了bar 函数?如果我是对的,那么堆栈跟踪应该说它在第 5 行、第 7 行、第 9 行暂停。
  • 我认为这与 V8 扁平化功能无关。我认为这只是一个怪癖。我不知道我是否会称它为错误。我认为大卫下面的回答最有意义。
  • 我有同样的问题,我讨厌它。但是当我需要在控制台中访问闭包条目时,我会转到可以看到范围的位置,找到 Closure 条目并打开它。然后右键单击您需要的元素并单击存储为全局变量。控制台附加了一个新的全局变量temp1,您可以使用它来访问范围条目。

标签: javascript google-chrome google-chrome-devtools


【解决方案1】:

我找到了一个 v8 issue report,这正是您所要求的。

现在,总结一下该问题报告中所说的内容... v8 可以将本地函数存储在堆栈上的“上下文”对象中,该对象位于堆上.只要函数不包含任何引用它们的内部函数,它将在堆栈上分配局部变量。 这是一种优化。如果 any 内部函数引用了一个局部变量,这个变量将被放入一个上下文对象中(即在堆上而不是在堆栈上)。 eval 的情况比较特殊:如果它完全被内部函数调用,所有局部变量都放在上下文对象中。

上下文对象的原因是,通常您可以从外部函数返回一个内部函数,然后外部函数运行时存在的堆栈将不再可用。因此,内部函数访问的任何内容都必须在外部函数中存在并存在于堆上而不是堆栈上。

调试器无法检查堆栈上的那些变量。关于调试中遇到的问题,一位项目成员says

我能想到的唯一解决方案是,只要打开 devtools,我们就会取消所有代码并使用强制上下文分配重新编译。但是,如果启用 devtools,这将大大降低性能。

这是一个“如果任何内部函数引用变量,则将其放入上下文对象”的示例。如果您运行它,您将能够在debugger 语句中访问x,即使x 仅用于foo 函数,从未调用过

function baz() {
  var x = "x value";
  var z = "z value";

  function foo () {
    console.log(x);
  }

  function bar() {
    debugger;
  };

  bar();
}
baz();

【讨论】:

  • 你找到deopt代码的方法了吗?我喜欢将调试器用作 REPL 和代码,然后将代码传输到我自己的文件中。但这通常是不可行的,因为应该存在的变量无法访问。一个简单的评估不会做到这一点。我听说一个无限循环可能。
  • 我在调试时实际上并没有遇到这个问题,所以我没有搜索deopt代码的方法。
  • 该问题的最后一条评论说:将 V8 置于强制分配上下文的模式是可能的,但我不确定如何/何时通过 Devtools UI 触发它 i> 为了调试,我有时会想这样做。我怎样才能强制这种模式?
  • @user208769 当关闭为重复时,我们倾向于对未来读者最有用的问题。有多种因素可以帮助确定哪个问题最有用:您的问题恰好得到 0 个答案,而这个问题得到了多个赞成的答案。所以这个问题是这两个问题中最有用的。只有在有用性基本相同时,日期才会成为决定因素。
  • 这个答案回答了实际问题(为什么?),但隐含的问题是——如何访问未使用的上下文变量进行调试,而不在我的代码中添加对它们的额外引用? -- 下面的@OwnageIsMagic 更好地回答了。
【解决方案2】:

就像@Louis 所说的那样,它是由 v8 优化引起的。 您可以遍历调用堆栈到该变量可见的帧:

或将debugger替换为

eval('debugger');

eval 将取消当前块

【讨论】:

  • 几乎很棒!它在内容为debugger 的VM 模块(黄色)中暂停,并且上下文确实可用。如果您将堆栈升级到您实际尝试调试的代码,您将回到无法访问上下文的状态。所以它有点笨拙,在访问隐藏的闭包变量时无法查看您正在调试的代码。不过,我会投赞成票,因为它使我不必添加显然不是用于调试的代码,并且它使我可以访问整个上下文而无需取消优化整个应用程序。
  • 哦...它甚至比必须使用黄色的evaled 源窗口来访问上下文更笨拙:您不能单步执行代码(除非您将 eval('debugger') 放在所有你想通过的行。)
  • 似乎有些情况下,某些变量即使在遍历到合适的栈帧后也是不可见的;我有类似controllers.forEach(c => c.update()) 的东西,并在c.update() 深处的某个地方打了一个断点。如果我然后选择调用controllers.forEach() 的框架,controllers 是未定义的(但该框架中的其他所有内容都是可见的)。我无法使用最小版本进行复制,我想可能需要通过一些复杂性阈值之类的。
  • @PeterT 如果它是 你是在错误的地方或somewhere deep inside c.update() 你的代码异步,你看到异步堆栈帧
【解决方案3】:

我在 nodejs 中也注意到了这一点。我相信(我承认这只是一个猜测)当代码编译时,如果x 没有出现在bar 中,它不会使xbar 的范围内可用。这可能会使其效率更高一些;问题是有人忘记(或不在乎)即使bar 中没有x,您可能决定运行调试器,因此仍然需要从bar 内部访问x

【讨论】:

  • 谢谢。基本上,我希望能够比“调试器的谎言”更好地向 javascript 初学者解释这一点。
  • @GabeKopley:从技术上讲,调试器没有撒谎。如果一个变量没有被引用,那么它在技术上不是封闭的。因此解释器不需要创建闭包。
  • 这不是重点。在使用调试器时,我经常遇到这样的情况,我想知道外部范围内变量的值,但因为这个原因我不能。从更哲学的角度来看,我会说调试器在撒谎。变量是否存在于内部范围不应取决于它是否实际使用或是否存在无关的eval 命令。如果变量被声明,它应该是可访问的。
【解决方案4】:

哇,真有趣!

正如其他人所提到的,这似乎与scope 有关,但更具体地说,与debugger scope 有关。在开发人员工具中评估注入的脚本时,它似乎确定了一个ScopeChain,这会导致一些古怪(因为它绑定到检查器/调试器范围)。您发布的内容的一个变体是这样的:

(编辑 - 实际上,您在原始问题中提到了这一点,哎呀,我的错!

function foo() {
  var x = "bat";
  var y = "man";

  function bar() {
    console.log(x); // logs "bat"

    debugger; // Attempting to access "y" throws the following
              // Uncaught ReferenceError: y is not defined
              // However, x is available in the scopeChain. Weird!
  }
  bar();
}
foo();

对于雄心勃勃和/或好奇的人,请查看(呵呵)源代码以查看发生了什么:

https://github.com/WebKit/webkit/tree/master/Source/JavaScriptCore/inspector https://github.com/WebKit/webkit/tree/master/Source/JavaScriptCore/debugger

【讨论】:

    【解决方案5】:

    我似乎可以访问_thisthis 在我的 chrome 检查器中未定义,_this 似乎引用了适当的上下文(并且可能是在堆栈跟踪检查器中用作 > local > this 的内容?)。

    【讨论】:

      【解决方案6】:

      我怀疑这与变量和函数提升有关。 JavaScript 将所有变量和函数声明带到定义它们的函数的顶部。更多信息在这里:http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/

      我敢打赌,Chrome 调用断点时变量对范围不可用,因为函数中没有其他内容。这似乎有效:

      function baz() {
        var x = "foo";
      
        function bar() {
          console.log(x); 
          debugger;
        };
        bar();
      }

      这样:

      function baz() {
        var x = "foo";
      
        function bar() {
          debugger;
          console.log(x);     
        };
        bar();
      }

      希望这个和/或上面的链接有所帮助。这些是我最喜欢的 SO 问题,顺便说一句:)

      【讨论】:

      • 谢谢! :) 我想知道 FF 的不同之处。从我作为开发者的角度来看,FF 的体验客观上更好......
      • “在 lex 时间调用断点”我对此表示怀疑。这不是断点的用途。而且我不明白为什么函数中没有其他东西很重要。话虽如此,如果它与 nodejs 类似,那么断点可能会非常错误。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-07-26
      • 2018-07-21
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多