Warum hält der Chrome-Debugger die geschlossene lokale Variable für undefiniert?

167

Mit diesem Code:

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

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

Ich bekomme dieses unerwartete Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Wenn ich den Code ändere:

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

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

Ich bekomme das erwartete Ergebnis:

Geben Sie hier die Bildbeschreibung ein

Wenn evalinnerhalb der inneren Funktion ein Aufruf erfolgt , kann ich auf meine Variable zugreifen, wie ich möchte (unabhängig davon, an was ich übergebe eval).

In der Zwischenzeit geben die Firefox-Entwicklungstools unter beiden Umständen das erwartete Verhalten wieder.

Was ist mit Chrome los, dass sich der Debugger weniger bequem verhält als Firefox? Ich habe dieses Verhalten seit einiger Zeit bis einschließlich Version 41.0.2272.43 Beta (64-Bit) beobachtet.

Ist es so, dass die Javascript-Engine von Chrome die Funktionen "abflacht", wenn dies möglich ist?

Interessanterweise ist die xVariable immer noch undefiniert, wenn ich eine zweite Variable hinzufüge, auf die in der inneren Funktion verwiesen wird.

Ich verstehe, dass es bei der Verwendung eines interaktiven Debuggers häufig Macken mit Umfang und Variablendefinition gibt, aber es scheint mir, dass es basierend auf der Sprachspezifikation eine "beste" Lösung für diese Macken geben sollte. Ich bin also sehr gespannt, ob dies daran liegt, dass Chrome weiter optimiert als Firefox. Und auch, ob diese Optimierungen während der Entwicklung leicht deaktiviert werden können oder nicht (sollten sie möglicherweise deaktiviert werden, wenn Entwickler-Tools geöffnet sind?).

Ich kann dies auch mit Haltepunkten sowie der debuggerAnweisung reproduzieren .

Gabe Kopley
quelle
2
Vielleicht werden nicht verwendete Variablen für Sie aus dem Weg
geräumt
markle976 scheint zu sagen, dass die debugger;Leitung nicht von innen aufgerufen wird bar. Schauen Sie sich also den Stack-Trace an, wenn er im Debugger angehalten wird: Wird die barFunktion im Stacktrace erwähnt? Wenn ich recht habe, sollte der Stacktrace sagen, dass er in Zeile 5, in Zeile 7, in Zeile 9 angehalten ist.
David Knipe
Ich glaube nicht, dass es etwas mit V8-Abflachungsfunktionen zu tun hat. Ich denke, das ist nur eine Eigenart. Ich weiß nicht, ob ich es überhaupt einen Fehler nennen würde. Ich denke, Davids Antwort unten ist am sinnvollsten.
Markle976
2
Ich habe das gleiche Problem, ich hasse es. Wenn ich jedoch Zugriffsschließungseinträge in der Konsole benötigen muss, gehe ich zu dem Bereich, in dem Sie den Bereich sehen können, finde den Abschlusseintrag und öffne ihn. Klicken Sie dann mit der rechten Maustaste auf das gewünschte Element und klicken Sie auf Als globale Variable speichern . Eine neue globale Variable temp1wird an die Konsole angehängt und Sie können damit auf den Bereichseintrag zugreifen.
Pablo

Antworten:

149

Ich habe einen v8-Problembericht gefunden , der genau das beschreibt, was Sie fragen.

Um zusammenzufassen, was in diesem Problembericht gesagt wird ... v8 kann die Variablen, die für eine Funktion lokal sind, auf dem Stapel oder in einem "Kontext" -Objekt speichern, das sich auf dem Heap befindet. Es werden lokale Variablen auf dem Stapel zugewiesen, solange die Funktion keine innere Funktion enthält, die auf sie verweist. Es ist eine Optimierung . Wenn sich eine innere Funktion auf eine lokale Variable bezieht, wird diese Variable in ein Kontextobjekt eingefügt (dh auf den Heap anstatt auf den Stapel). Der Fall von evalist etwas Besonderes: Wenn er überhaupt von einer inneren Funktion aufgerufen wird, werden alle lokalen Variablen in das Kontextobjekt eingefügt.

Der Grund für das Kontextobjekt ist, dass Sie im Allgemeinen eine innere Funktion von der äußeren zurückgeben können und dann der Stapel, der während der Ausführung der äußeren Funktion vorhanden war, nicht mehr verfügbar ist. Alles, worauf die innere Funktion zugreift, muss die äußere Funktion überleben und auf dem Haufen und nicht auf dem Stapel leben.

Der Debugger kann die auf dem Stapel befindlichen Variablen nicht überprüfen. In Bezug auf das Problem beim Debuggen sagt ein Projektmitglied :

Die einzige Lösung, die ich mir vorstellen kann, ist, dass wir bei aktivierten devtools den gesamten Code deoptieren und mit erzwungener Kontextzuweisung neu kompilieren. Dies würde die Leistung bei aktivierten devtools jedoch dramatisch beeinträchtigen.

Hier ist ein Beispiel für "Wenn sich eine innere Funktion auf die Variable bezieht, fügen Sie sie in ein Kontextobjekt ein". Wenn Sie dies ausführen, können Sie xauf die debuggerAnweisung zugreifen , obwohl sie xnur in der fooFunktion verwendet wird, die niemals aufgerufen wird !

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();
Louis
quelle
13
Haben Sie einen Weg gefunden, Code zu deoptieren? Ich verwende den Debugger gerne als REPL und Code dort und übertrage den Code dann in meine eigenen Dateien. Aber es ist oft nicht machbar, da Variablen, die dort sein sollten, nicht zugänglich sind. Eine einfache Bewertung reicht nicht aus. Ich höre eine unendliche for-Schleife.
Ray Foss
Ich bin beim Debuggen nicht auf dieses Problem gestoßen, daher habe ich nicht nach Möglichkeiten gesucht, Code zu deoptieren.
Louis
6
Der letzte Kommentar aus dem Problem lautet: Es ist möglich, V8 in einen Modus zu versetzen, in dem alles zwangsweise dem Kontext zugewiesen wird, aber ich bin nicht sicher, wie / wann dies über die Devtools-Benutzeroberfläche ausgelöst werden soll. Zum Debuggen möchte ich dies manchmal tun . Wie kann ich einen solchen Modus erzwingen?
Suma
2
@ user208769 Beim Schließen als Duplikat bevorzugen wir die Frage, die für zukünftige Leser am nützlichsten ist. Es gibt mehrere Faktoren, die bestimmen, welche Frage am nützlichsten ist: Ihre Frage hat genau 0 Antworten erhalten, während diese Frage mehrere Antworten erhalten hat. Diese Frage ist also die nützlichste von beiden. Daten werden nur dann zu einem bestimmenden Faktor, wenn der Nutzen größtenteils gleich ist.
Louis
1
Diese Antwort beantwortet die eigentliche Frage (Warum?), Aber die implizite Frage: Wie erhalte ich Zugriff auf nicht verwendete Kontextvariablen zum Debuggen, ohne zusätzliche Verweise auf sie in meinem Code hinzuzufügen? - wird besser von @OwnageIsMagic unten beantwortet.
Sigfried
30

Wie @Louis sagte, wurde es durch v8-Optimierungen verursacht. Sie können den Aufrufstapel zu einem Frame durchlaufen, in dem diese Variable sichtbar ist:

call1 call2

Oder ersetzen debuggerdurch

eval('debugger');

eval wird den aktuellen Block deaktivieren

OwnageIsMagic
quelle
1
Fast großartig! Es wird in einem VM-Modul (gelb) mit Inhalten angehalten debugger, und der Kontext ist tatsächlich verfügbar. Wenn Sie den Stapel um eine Ebene auf den Code erhöhen, den Sie tatsächlich debuggen möchten, haben Sie wieder keinen Zugriff auf den Kontext. Es ist also nur ein bisschen klobig, nicht in der Lage zu sein, den Code, den Sie debuggen, beim Zugriff auf versteckte Schließungsvariablen anzuzeigen. Ich werde mich jedoch dafür entscheiden, da ich keinen Code hinzufügen muss, der nicht offensichtlich zum Debuggen geeignet ist, und auf den gesamten Kontext zugreifen kann, ohne die gesamte App zu deoptimieren.
Sigfried
Oh ... es ist noch umständlicher, als das gelbe Quellfenster verwenden zu müssen, evalum Zugriff auf den Kontext zu erhalten: Sie können den Code nicht schrittweise durchlaufen (es sei denn, Sie setzen eval('debugger')zwischen alle Zeilen, die Sie durchlaufen möchten).
Sigfried
Es scheint Situationen zu geben, in denen bestimmte Variablen auch nach dem Durchlaufen des entsprechenden Stapelrahmens unsichtbar sind. Ich habe so etwas wie controllers.forEach(c => c.update())und habe irgendwo tief drinnen einen Haltepunkt erreicht c.update(). Wenn ich dann den Frame auswähle, in dem controllers.forEach()aufgerufen wird, controllersist er undefiniert (aber alles andere in diesem Frame ist sichtbar). Ich konnte nicht mit einer minimalen Version reproduzieren, ich nehme an, dass es eine Komplexitätsschwelle gibt, die überschritten werden muss oder so.
PeterT
@ PeterT, wenn es <undefined> ist, sind Sie an der falschen Stelle oder somewhere deep inside c.update()Ihr Code wird asynchron und Sie sehen
asynchronen Stapelrahmen
6

Ich habe dies auch in nodejs bemerkt. Ich glaube (und ich gebe zu, dass dies nur eine Vermutung ist), dass der Code, wenn er kompiliert wird, wenn er xnicht im Inneren erscheint bar, nicht xim Rahmen von verfügbar gemacht wird bar. Dies macht es wahrscheinlich etwas effizienter; das Problem ist , jemand vergessen (oder Pflege nicht) , dass selbst wenn es keine ist xin bar, der Debugger laufen und damit noch brauchen , um Zugang können entscheiden , ob xvon innen bar.

David Knipe
quelle
2
Vielen Dank. Grundsätzlich möchte ich dies Javascript-Anfängern besser erklären können als "Der Debugger lügt".
Gabe Kopley
@GabeKopley: Technisch gesehen lügt der Debugger nicht. Wenn auf eine Variable nicht verwiesen wird, ist sie technisch nicht eingeschlossen. Somit ist es nicht erforderlich, dass der Dolmetscher den Verschluss erstellt.
Slebetman
7
Das ist nicht der Punkt. Bei der Verwendung des Debuggers war ich häufig in einer Situation, in der ich den Wert einer Variablen in einem äußeren Bereich wissen wollte, dies jedoch nicht konnte. Und philosophischer würde ich sagen, dass der Debugger lügt. Ob die Variable im inneren Bereich vorhanden ist, sollte nicht davon abhängen, ob sie tatsächlich verwendet wird oder ob es einen nicht verwandten evalBefehl gibt. Wenn die Variable deklariert ist, sollte sie zugänglich sein.
David Knipe
2

Wow, wirklich interessant!

Wie andere erwähnt haben, scheint dies damit zu tun zu haben scope, aber insbesondere damit debugger scope. Wenn injiziertes Skript in den Entwicklertools ausgewertet wird, scheint es a zu bestimmenScopeChain , was zu einer gewissen Eigenart führt (da es an den Inspector / Debugger-Bereich gebunden ist). Eine Variation von dem, was Sie gepostet haben, ist folgende:

(BEARBEITEN - eigentlich erwähnen Sie dies in Ihrer ursprünglichen Frage, yikes, mein schlechtes! )

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

Für die Ehrgeizigen und / oder Neugierigen (heh) suchen Sie die Quelle heraus, um zu sehen, was los ist:

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

Jack
quelle
0

Ich vermute, dass dies mit dem Heben von Variablen und Funktionen zu tun hat. JavaScript bringt alle Variablen- und Funktionsdeklarationen an den Anfang der Funktion, in der sie definiert sind. Weitere Informationen finden Sie hier: http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/

Ich wette, Chrome ruft den Haltepunkt mit der Variablen auf, die für den Bereich nicht verfügbar ist, da die Funktion nichts anderes enthält. Das scheint zu funktionieren:

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

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

Wie das:

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

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

Hoffe das und / oder der obige Link hilft. Dies sind meine Lieblings-SO-Fragen, übrigens :)

markle976
quelle
Vielen Dank! :) Ich frage mich, was FF anders macht. Aus meiner Sicht als Entwickler ist die FF-Erfahrung objektiv besser ...
Gabe Kopley
2
"Den Haltepunkt zur Lex-Zeit anrufen" Ich bezweifle es. Dafür gibt es keine Haltepunkte. Und ich verstehe nicht, warum das Fehlen anderer Dinge in der Funktion von Bedeutung sein sollte. Allerdings können Haltepunkte sehr fehlerhaft sein, wenn es sich um NodeJS handelt.
David Knipe