为什么Chrome调试器认为已关闭的本地变量未定义?

192

使用以下代码:

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

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

我得到了这个意外的结果:

在此输入图像描述

当我更改代码时:

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

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

我得到了期望的结果:

enter image description here

而且,如果内部函数中有任何对 eval 的调用,我可以按照自己的意愿访问我的变量(无论我传递什么给 eval)。

与此同时,Firefox 开发工具在这两种情况下都给出了期望的行为。

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

是不是 Chrome 的 JavaScript 引擎会在可能的情况下“展开”函数?

有趣的是,如果我添加第二个变量,并且内部函数确实引用了它,那么 x 变量仍然是未定义的。

我知道,在使用交互式调试器时,作用域和变量定义通常会有些怪异之处,但根据语言规范,似乎应该有一个“最佳”的解决方案来处理这些怪异之处。因此,我非常好奇这是否是因为 Chrome 进行了比 Firefox 更深层次的优化。而且这些优化在开发期间是否可以轻松禁用(也许当开发工具打开时它们应该被禁用?)。

此外,我可以使用断点和 debugger 语句重现这个问题。


2
也许它会为你清除未使用的变量... - dandavis
markle976似乎在说debugger;这一行并没有从bar函数内部调用。因此,在调试器中暂停时查看堆栈跟踪:堆栈跟踪中是否提到了bar函数?如果我是正确的,那么堆栈跟踪应该显示它在第5行、第7行、第9行暂停。 - David Knipe
2
我有同样的问题,我很讨厌它。但是当我需要在控制台中访问闭包条目时,我会去到可以看到作用域的地方,找到_Closure_条目并打开它。然后右键单击所需元素并单击_Store as Global Variable_。一个新的全局变量temp1附加到控制台上,您可以使用它来访问作用域条目。 - Pablo
这对我来说似乎非常合理;变量没有被函数bar使用,因此在bar的执行上下文中,它对调试器不可见,因为运行时没有义务在该上下文中保留/使不必要的事物可见。 - Lawrence Dol
显示剩余3条评论
7个回答

169

我找到一个v8 问题报告,准确地解释了你所问的内容。

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

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

调试器无法检查存储在堆栈上的变量。关于调试时遇到的问题,一位项目成员表示

我能想到的唯一解决办法是每次devtools打开时,我们都会取消所有代码并重新编译强制上下文分配。但是,这将大大降低开启devtools时的性能。

以下是“如果任何内部函数引用变量,则将其放在上下文对象中”的示例。如果运行此代码,则可以在debugger语句处访问x,即使只有在foo函数中使用了x,而该函数从未被调用

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();

16
你有找到反优化代码的方法吗?我喜欢将调试器当作 REPL 来使用,在那里编写代码,然后将代码转移到自己的文件中。但通常情况下这是行不通的,因为应该存在的变量是不可访问的。简单的 eval 不能解决这个问题。我听说一个无限 for 循环可能会起作用。 - Ray Foss
我在调试过程中并没有遇到这个问题,所以我也没有寻找去优化代码的方法。 - Louis
8
问题的最后一个评论说:“将V8放入强制上下文分配的模式中是可能的,但我不确定如何/何时通过Devtools UI触发它。”出于调试的目的,有时我想这样做。我该如何强制启用这种模式? - Suma
@Louis:作为参考,这个问题是重复的。 - user208769
2
@user208769 当我们将问题标记为重复时,我们更倾向于选择对未来读者最有用的问题。有多个因素可以帮助确定哪个问题最有用:您的问题没有得到任何答案,而这个问题却得到了多个赞同的答案。因此,这个问题是两个问题中最有用的。只有在有用性大致相等的情况下,日期才成为一个决定性因素。 - Louis
2
这个答案回答了实际问题(为什么?),但是隐含的问题——如何在调试时访问未使用的上下文变量而不在代码中添加额外引用?——最好由下面的@OwnageIsMagic回答。 - Sigfried

36

正如 @Louis 所说,这是由于 v8 的优化引起的。您可以遍历调用堆栈到可见此变量的帧:

call1 call2

或将 debugger 替换为

eval('debugger');

eval 会使当前代码块失去优化


1
几乎很好!它在一个VM模块(黄色)中暂停,内容为“debugger”,并且确实可用上下文。如果您将堆栈向上移动一级到您实际尝试调试的代码,则无法访问上下文,因此有点笨拙。但是我会点赞,因为它使我不必添加显然不是用于调试的代码,并且可以访问整个上下文而不会使整个应用程序失去优化。 - Sigfried
哦...它甚至比使用黄色的被 eval 的源窗口来获取上下文还要笨拙:你无法逐步执行代码(除非在要逐步执行的所有行之间放置 eval('debugger')). - Sigfried
似乎存在某些情况,即使遍历到了适当的堆栈帧,某些变量仍然是不可见的;我有类似于 controllers.forEach(c => c.update()) 的东西,并在 c.update() 深处的某个断点处停止。如果我选择调用 controllers.forEach() 的帧,则 controllers 为未定义(但该帧中的其他所有内容都可见)。我无法使用最小版本进行复制,我想可能需要通过某种复杂性阈值或其他方式来解决。 - PeterT
@PeterT 如果它是<undefined>,那么你就错了地方了。 或者,在c.update()的深处,你的代码变成了异步,你会看到异步堆栈帧。 - OwnageIsMagic

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

3
谢谢。基本上,我希望您能比“调试器是错的”更好地向JavaScript初学者解释这个问题。 - Gabe Kopley
2
@GabeKopley:从技术上讲,调试器并没有说谎。如果一个变量没有被引用,那么它就不是在技术上被封闭的。因此,解释器没有必要创建闭包。 - slebetman
7
这不是重点。使用调试器时,我经常遇到这样的情况:想知道外部作用域中变量的值,但由于某些原因无法获取。从更哲学的角度来看,我会说调试器在撒谎。变量是否存在于内部作用域中,不应该取决于它是否被使用或是否有一个无关的eval命令。如果变量已声明,就应该可访问。 - David Knipe
1
没有人承诺你可以将一个变量自动提升为可在闭包中访问。给了一点,人们就会期望更多。 - doug65536
没有人在“推广”任何东西。我没有查阅JavaScript规范,因为生命太短暂了,但我认为它说闭包变量是有效的。我还假设它没有说不实现未使用的变量是可以的。这只是一个实现细节,而不是规范应该关注的内容。 - David Knipe

2

哇,真的很有趣!

正如其他人所提到的那样,这似乎与 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

请注意,这些链接提供了Webkit中JavaScriptCore的检查器和调试器源代码。

1
我怀疑这与变量和函数提升有关。JavaScript将所有变量和函数声明提升到它们所定义的函数的顶部。更多信息在这里:http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/ 我打赌Chrome在没有其他函数的情况下调用带有不可用于该范围的变量的断点。这似乎有效:

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

  function bar() {
    console.log(x); 
    debugger;
  };
  bar();
}

As does this:

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

  function bar() {
    debugger;
    console.log(x);     
  };
  bar();
}

希望这个链接能够帮到你,这些是我最喜欢的 Stack Overflow 问题类型,顺便说一下 :)

1
谢谢! :) 我想知道FF有什么不同。作为一名开发人员,从我的角度来看,FF的体验是客观上更好的... - Gabe Kopley
2
在词法分析时调用断点?我怀疑这一点。断点不是为此而设计的。我也不明白函数中缺少其他内容为什么会有影响。话虽如此,如果它类似于Node.js,那么断点可能会非常有bug。 - David Knipe

1

我好像可以访问_this。在Chrome检查器中,this对我来说是未定义的,_this似乎引用了适当的上下文(并且可能是堆栈跟踪检查器中用作> local > this的东西?)。


0

我知道这可能有点老了,但我的问题是使用babel进行代码压缩 - 即--presets minify

当我的js代码被构建并且被压缩时,我的本地变量变成了未定义; 当没有压缩时,我可以在控制台中看到变量的值。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接