Chrome调试器为何认为封闭的局部变量未定义?


167

使用此代码:

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语句来重现此内容。


2
也许它会为您
省去一些

markle976似乎在说debugger;实际上不是从内部调用该行bar。因此,在调试器中暂停时查看堆栈跟踪:在堆栈跟踪中是否bar提到了该函数?如果我是正确的,那么堆栈跟踪应该说这是停在5号线,在第7行,在第九行
大卫Knipe

我认为这与V8展平功能无关。我认为这只是一个怪癖;我不知道我是否会称它为错误。我认为以下大卫的答案是最合理的。
markle976'2


2
我有同样的问题,我讨厌。但是,当我需要在控制台中具有访问关闭条目时,我转到可以看到作用域的位置,找到“ 关闭”条目并打开它。然后右键单击所需的元素,然后单击“ 存储为全局变量”。新的全局变量temp1将附加到控制台,您可以使用它来访问作用域条目。
巴勃罗(Pablo)

Answers:


149

我找到了一份v8 问题报告,该报告正是您的要求。

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

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

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

我能想到的唯一解决方案是,每当打开devtools时,我们将取消所有代码并使用强制上下文分配重新编译。但是,启用devtools可以大大降低性能。

这是“如果内部函数引用了变量,则将其放在上下文对象中”的示例。如果运行此命令x,则debugger即使x仅在foo函数中使用该语句,也可以对其进行访问,而该函数永远不会调用

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();

13
您是否找到了一种取消代码的方法?我喜欢将调试器用作REPL,然后在其中进行编码,然后将代码传输到我自己的文件中。但这通常是不可行的,因为应该存在的变量无法访问。一个简单的评估不会做。我听到无限循环的可能。
Ray Foss

我在调试时实际上并没有遇到这个问题,所以我没有寻找取消代码的方法。
路易斯

6
问题的最后一条评论说:可以将V8置于强制所有内容都在上下文中分配的模式,但是我不确定如何/何时通过Devtools UI触发它,为了调试,我有时会想要这样做。如何强制这种模式?
Suma

2
@ user208769当重复关闭时,我们倾向于对以后的读者最有用的问题。有多种因素可以帮助您确定哪个问题最有用:您的问题恰好有0个答案,而这个问题有多个赞成答案。因此,这个问题是两者中最有用的。只有在有用性基本相等的情况下,日期才成为决定因素。
路易(Louis)

1
这个答案回答了实际的问题(为什么?),但是隐含的问题-如何在未在代码中添加对它们的额外引用的情况下访问未使用的上下文变量进行调试?-最好由下面的@OwnageIsMagic回答。
Sigfried

30

就像@Louis所说,它是由v8优化引起的。您可以遍历“调用”堆栈以显示该变量所在的帧:

通话1 通话2

或替换debugger

eval('debugger');

eval 将取消当前块


1
差不多了!它会在包含content的VM模块(黄色)中暂停debugger,并且上下文确实可用。如果将堆栈提升到您实际尝试调试的代码的一级,您将回到无法访问上下文的状态。因此,这有点笨拙,在访问隐藏的闭包变量时无法查看正在调试的代码。不过,我会投票赞成,因为它使我不必添加显然不是用于调试的代码,并且可以在不优化整个应用程序的情况下访问整个上下文。
Sigfried

哦...比使用黄色evaled源窗口访问上下文还要笨拙:您无法单步执行代码(除非您将eval('debugger')所有单行插入所有想要执行的行之间。)
Sigfried

似乎在某些情况下,即使遍历到适当的堆栈框架,某些变量也不可见;我有一个类似的东西controllers.forEach(c => c.update()),在内部深处碰到一个断点c.update()。如果然后选择在其中controllers.forEach()调用的框架,则该框架controllers是不确定的(但是该框架中的所有其他内容都是可见的)。我无法使用最低版本进行复制,我想可能存在一些需要传递的复杂性阈值。
PeterT

@PeterT,如果它是<undefined>,则您放置在错误的位置,或者somewhere deep inside c.update()您的代码进入异步状态,并且看到异步堆栈帧
OwnageIsMagic

6

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


2
谢谢。基本上,我希望能够比“调试器所在”更好地向javascript初学者解释这一点。
·科普利2015年

@GabeKopley:从技术上讲调试器不是在撒谎。如果未引用变量,则该变量在技术上不会包含在内。因此,不需要解释器来创建闭包。
slebetman '17

7
那不是重点。使用调试器时,我经常遇到一种情况,我想知道外部作用域中变量的值,但由于这个原因无法。从哲学的角度来说,我会说调试器在说谎。变量是否在内部作用域中不应该取决于它是否实际使用或是否存在不相关的eval命令。如果声明了变量,则应该可以访问。
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


0

我怀疑这与变量和函数提升有关。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的体验在客观上要更好……
Gabe Kopley 2015年

2
我怀疑“在lex时间调用断点”。那不是断点的目的。而且我不明白为什么函数中没有其他内容很重要。话虽如此,如果它类似于nodejs,那么断点可能会非常麻烦。
大卫·克尼普
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.