为什么 Chrome 调试器认为关闭的局部变量未定义?

IT技术 javascript google-chrome google-chrome-devtools
2021-01-30 10:17:35

使用此代码:

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

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

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

在此处输入图片说明

当我更改代码时:

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

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

我得到了预期的结果:

在此处输入图片说明

此外,如果eval在内部函数中有任何调用,我可以按照我的意愿访问我的变量(与我传递给 的内容无关eval)。

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

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

Chrome 的 javascript 引擎是否在可能的情况下“扁平化”了这些功能?

有趣的是,如果我添加第二可变在内部功能引用,则x变量仍然不确定。

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

此外,我可以使用断点和debugger语句重现这一点

6个回答

我找到了一个 v8问题报告,它正是关于你要问的。

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

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

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

我能想到的唯一解决方案是,无论何时启用 devtools,我们都会取消选择所有代码并使用强制上下文分配重新编译。但是,如果启用了 devtools,那将显着降低性能。

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

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();
@user208769 当关闭重复时,我们倾向于对未来读者最有用的问题。有多种因素可以帮助确定哪个问题最有用:您的问题得到了 0 个答案,而这个问题得到了多个赞成的答案。所以这个问题是两个中最有用的。仅当实用性基本相同时,日期才成为决定性因素。
2021-03-14 10:17:35
这个答案回答了实际问题(为什么?),但隐含的问题是——如何访问未使用的上下文变量进行调试,而无需在我的代码中添加对它们的额外引用?-- 下面的@OwnageIsMagic 给出了更好的回答。
2021-03-17 10:17:35
该问题的最后一条评论说:将 V8 置于强制分配上下文的模式是可能的,但我不确定如何/何时通过 Devtools UI 触发为了调试,我有时会想要这样做. 我怎样才能强制这种模式?
2021-03-18 10:17:35
我实际上并没有在调试时遇到这个问题,所以我没有寻找取消代码的方法。
2021-03-19 10:17:35
你有没有想出一种方法来取消代码?我喜欢将调试器用作 REPL 并在那里编写代码,然后将代码传输到我自己的文件中。但这通常是不可行的,因为应该存在的变量无法访问。简单的 eval 不会这样做。我听到无限循环可能。
2021-03-31 10:17:35

就像@Louis 所说的那样,它是由 v8 优化引起的。您可以遍历调用堆栈来确定此变量可见的位置:

呼叫 1 呼叫 2

或者替换debugger

eval('debugger');

eval 将取消当前块

@PeterT 如果它是 <undefined> 你在错误的地方或者somewhere deep inside c.update()你的代码异步并且你看到异步堆栈帧
2021-03-10 10:17:35
几乎很棒!它在包含内容的 VM module(黄色)中暂停debugger,并且上下文确实可用。如果您将堆栈提升一级到您实际尝试调试的代码,您将无法访问上下文。所以它只是有点笨拙,在访问隐藏的闭包变量时无法查看正在调试的代码。不过,我会赞成,因为它使我不必添加明显不用于调试的代码,并且它使我可以访问整个上下文,而无需取消优化整个应用程序。
2021-03-15 10:17:35
似乎有些情况即使遍历到适当的堆栈帧后某些变量也是不可见的;我有类似的东西controllers.forEach(c => c.update())并且在内部深处的某个地方遇到了断点c.update()如果我然后选择controllers.forEach()被调用的框架controllers则未定义(但该框架中的其他所有内容都是可见的)。我无法用最小版本重现,我想可能需要通过一些复杂性阈值或其他东西。
2021-03-16 10:17:35
哦...它甚至比必须使用黄色evaled 源窗口来访问上下文还要笨拙:你不能单步执行代码(除非你把eval('debugger')所有要单步执行的行放在中间。)
2021-03-31 10:17:35

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

谢谢。基本上,我希望能够比“调试器说谎”更好地向 javascript 初学者解释这一点。
2021-03-13 10:17:35
@GabeKopley:从技术上讲,调试器没有说谎。如果一个变量没有被引用,那么它在技术上就不是封闭的。因此解释器不需要创建闭包。
2021-03-19 10:17:35
这不是重点。在使用调试器时,我经常遇到这样一种情况:我想知道外部作用域中变量的值,但因此无法知道。从更哲学的角度来说,我会说调试器在撒谎。变量是否存在于内部作用域不应取决于它是否被实际使用或是否存在不相关的eval命令。如果声明了变量,它应该是可访问的。
2021-03-23 10:17:35

哇,真的很有趣!

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

(编辑 - 实际上,您在最初的问题中提到了这一点,oop,我的错!

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();

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

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

我似乎可以访问_this. this对我来说,chrome 检查器中哪里未定义,_this似乎引用了适当的上下文(并且可能是> local > this堆栈跟踪检查器中使用的上下文?)。