Pourquoi le débogueur Chrome pense-t-il que la variable locale fermée n'est pas définie?

167

Avec ce code:

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

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

J'obtiens ce résultat inattendu:

entrez la description de l'image ici

Quand je change le code:

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

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

J'obtiens le résultat attendu:

entrez la description de l'image ici

De plus, s'il y a un appel à evall'intérieur de la fonction interne, je peux accéder à ma variable comme je veux (peu importe ce à quoi je passe eval).

Pendant ce temps, les outils de développement de Firefox donnent le comportement attendu dans les deux cas.

Que se passe-t-il avec Chrome que le débogueur se comporte moins facilement que Firefox? J'ai observé ce comportement pendant un certain temps, jusqu'à la version 41.0.2272.43 bêta (64 bits) incluse.

Est-ce que le moteur javascript de Chrome "aplatit" les fonctions quand il le peut?

Fait intéressant si j'ajoute une deuxième variable qui est référencé dans la fonction intérieure, la xvariable est encore mal défini.

Je comprends qu'il y a souvent des bizarreries avec la portée et la définition de variable lors de l'utilisation d'un débogueur interactif, mais il me semble que sur la base de la spécification du langage, il devrait y avoir une «meilleure» solution à ces bizarreries. Je suis donc très curieux de savoir si cela est dû à l'optimisation de Chrome plus loin que Firefox. Et aussi si ces optimisations peuvent ou non être facilement désactivées pendant le développement (peut-être devraient-elles être désactivées lorsque les outils de développement sont ouverts?).

En outre, je peux reproduire cela avec des points d'arrêt ainsi que la debuggerdéclaration.

Gabe Kopley
la source
2
peut-être que cela vous
gêne les
markle976 semble dire que la debugger;ligne n'est pas réellement appelée de l'intérieur bar. Regardez donc la trace de la pile lorsqu'elle s'arrête dans le débogueur: la barfonction est-elle mentionnée dans la trace de pile? Si j'ai raison, alors le stacktrace devrait indiquer qu'il est mis en pause à la ligne 5, à la ligne 7, à la ligne 9.
David Knipe
Je ne pense pas que cela ait quoi que ce soit à voir avec les fonctions d'aplatissement du V8. Je pense que c'est juste une bizarrerie; Je ne sais pas si j'appellerais ça un bug. Je pense que la réponse de David ci-dessous est la plus logique.
markle976
2
J'ai le même problème, je déteste ça. Mais quand j'ai besoin d'avoir accès aux entrées de fermeture dans la console, je vais là où vous pouvez voir la portée, trouver l' entrée de fermeture et l'ouvrir. Ensuite, faites un clic droit sur l'élément dont vous avez besoin et cliquez sur Store as Global Variable . Une nouvelle variable globale temp1est attachée à la console et vous pouvez l'utiliser pour accéder à l'entrée d'étendue.
Pablo

Réponses:

149

J'ai trouvé un rapport de problème v8 qui concerne précisément ce que vous demandez.

Maintenant, pour résumer ce qui est dit dans ce rapport de problème ... v8 peut stocker les variables qui sont locales à une fonction sur la pile ou dans un objet "context" qui vit sur le tas. Il allouera des variables locales sur la pile tant que la fonction ne contient aucune fonction interne qui y fait référence. C'est une optimisation . Si une fonction interne fait référence à une variable locale, cette variable sera placée dans un objet de contexte (c'est-à-dire sur le tas au lieu de sur la pile). Le cas de evalest particulier: s'il est appelé par une fonction interne, toutes les variables locales sont placées dans l'objet context.

La raison de l'objet context est qu'en général, vous pouvez renvoyer une fonction interne à partir de la fonction externe, puis la pile qui existait pendant l'exécution de la fonction externe ne sera plus disponible. Ainsi, tout ce à quoi la fonction interne accède doit survivre à la fonction externe et vivre sur le tas plutôt que sur la pile.

Le débogueur ne peut pas inspecter ces variables qui se trouvent sur la pile. Concernant le problème rencontré lors du débogage, un membre du projet dit :

La seule solution à laquelle je pourrais penser est que chaque fois que devtools est activé, nous désoptons tout le code et le recompilons avec une allocation de contexte forcée. Cela réduirait considérablement les performances avec les outils de développement activés.

Voici un exemple de "si une fonction interne fait référence à la variable, placez-la dans un objet de contexte". Si vous exécutez ceci, vous pourrez accéder xà l' debuggerinstruction même si elle xn'est utilisée que dans la foofonction, qui n'est jamais appelée !

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();
Louis
la source
13
Avez-vous trouvé un moyen de désactiver le code? J'aime utiliser le débogueur comme REPL et y coder puis transférer le code dans mes propres fichiers. Mais ce n'est souvent pas faisable car les variables qui devraient être là ne sont pas accessibles. Une simple évaluation ne le fera pas. J'entends une boucle for infinie.
Ray Foss
Je n'ai pas réellement rencontré ce problème lors du débogage, donc je n'ai pas cherché de moyens de désélectionner le code.
Louis
6
Le dernier commentaire du problème dit: Mettre V8 dans un mode où tout est alloué de force par le contexte est possible, mais je ne sais pas comment / quand le déclencher via l'interface utilisateur de Devtools Pour des raisons de débogage, je voudrais parfois le faire . Comment puis-je forcer un tel mode?
Suma
2
@ user208769 Lors de la fermeture en double, nous privilégions la question la plus utile aux futurs lecteurs. Plusieurs facteurs aident à déterminer quelle question est la plus utile: votre question a obtenu exactement 0 réponse tandis que celle-ci a obtenu plusieurs réponses positives. Cette question est donc la plus utile des deux. Les dates ne deviennent un facteur déterminant que si l'utilité est généralement égale.
Louis
1
Cette réponse répond à la question réelle (Pourquoi?), Mais à la question implicite - Comment puis-je accéder aux variables de contexte inutilisées pour le débogage sans y ajouter de références supplémentaires dans mon code? - est mieux répondu par @OwnageIsMagic ci-dessous.
Sigfried
30

Comme @Louis l'a dit, cela est dû aux optimisations v8. Vous pouvez parcourir la pile des appels vers le cadre où cette variable est visible:

appeler1 appeler2

Ou remplacer debuggerpar

eval('debugger');

eval désactive le bloc actuel

PosséderIsMagique
la source
1
Presque génial! Il s'arrête dans un module VM (jaune) avec le contenu debugger, et le contexte est en effet disponible. Si vous augmentez d'un niveau la pile vers le code que vous essayez réellement de déboguer, vous n'avez plus accès au contexte. Donc, c'est juste un peu maladroit, ne pas pouvoir regarder le code que vous déboguez tout en accédant aux variables de fermeture cachées. Je vais cependant voter pour, car cela m'évite d'avoir à ajouter du code qui n'est évidemment pas destiné au débogage, et cela me donne accès à tout le contexte sans désoptimiser toute l'application.
Sigfried
Oh ... c'est encore plus maladroit que d'avoir à utiliser la evalfenêtre source jaune ed pour accéder au contexte: vous ne pouvez pas parcourir le code (à moins que vous ne mettiez eval('debugger')entre toutes les lignes que vous souhaitez parcourir.)
Sigfried
Il semble qu'il y ait des situations où certaines variables sont invisibles même après avoir traversé le cadre de pile approprié; J'ai quelque chose comme controllers.forEach(c => c.update())et j'ai atteint un point d'arrêt quelque part au fond de moi c.update(). Si je sélectionne ensuite le cadre où controllers.forEach()est appelé, il controllersn'est pas défini (mais tout le reste de ce cadre est visible). Je ne pourrais pas reproduire avec une version minimale, je suppose qu'il peut y avoir un certain seuil de complexité à franchir ou quelque chose du genre.
PeterT
@PeterT s'il est <non défini> vous êtes au mauvais endroit Ou somewhere deep inside c.update()votre code devient asynchrone et vous voyez un cadre de pile asynchrone
OwnageIsMagic
6

J'ai également remarqué cela dans nodejs. Je crois (et j'admets que ce n'est qu'une supposition) que lorsque le code est compilé, s'il xn'apparaît pas à l'intérieur bar, il ne le rend pas xdisponible dans la portée de bar. Cela le rend probablement un peu plus efficace; le problème est que quelqu'un a oublié (ou s'en moque) que même s'il n'y a pas d' xentrée bar, vous pourriez décider d'exécuter le débogueur et donc toujours avoir besoin d'accéder xde l'intérieur bar.

David Knipe
la source
3
Merci. Fondamentalement, je veux être en mesure d'expliquer cela aux débutants javascript mieux que "Le débogueur ment".
Gabe Kopley
@GabeKopley: Techniquement, le débogueur ne ment pas. Si une variable n'est pas référencée, elle n'est pas techniquement incluse. Ainsi, l'interprète n'a pas besoin de créer la fermeture.
slebetman
7
Ce n'est pas le propos. Lors de l'utilisation du débogueur, j'ai souvent été dans une situation où je voulais connaître la valeur d'une variable dans une portée externe, mais je ne pouvais pas à cause de cela. Et sur une note plus philosophique, je dirais que le débogueur ment. L'existence de la variable dans la portée interne ne devrait pas dépendre de son utilisation réelle ou de l'existence d'une evalcommande non liée . Si la variable est déclarée, elle doit être accessible.
David Knipe
2

Wow, vraiment intéressant!

Comme d'autres l'ont mentionné, cela semble être lié scope, mais plus spécifiquement, lié à debugger scope. Lorsque le script injecté est évalué dans les outils de développement, il semble déterminer a ScopeChain, ce qui entraîne une certaine bizarrerie (car il est lié à la portée de l'inspecteur / débogueur). Voici une variante de ce que vous avez publié:

(EDIT - en fait, vous mentionnez cela dans votre question initiale, ouais, mon mauvais! )

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

Pour les ambitieux et / ou curieux, explorez (heh) la source pour voir ce qui se passe:

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

Jack
la source
0

Je soupçonne que cela a à voir avec le levage de variables et de fonctions. JavaScript amène toutes les déclarations de variables et de fonctions en haut de la fonction dans laquelle elles sont définies. Plus d'informations ici: http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/

Je parie que Chrome appelle le point de rupture avec la variable indisponible pour la portée car il n'y a rien d'autre dans la fonction. Cela semble fonctionner:

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

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

Comme ça:

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

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

J'espère que cela et / ou le lien ci-dessus vous aidera. Ce sont mes types de questions SO préférées, BTW :)

markle976
la source
Merci! :) Je me demande ce que FF fait différemment. De mon point de vue en tant que développeur, l'expérience FF est objectivement meilleure ...
Gabe Kopley
2
"appeler le point de rupture à l'heure de lex" J'en doute. Ce n'est pas à cela que servent les points d'arrêt. Et je ne vois pas pourquoi l'absence d'autres choses dans la fonction devrait avoir de l'importance. Cela dit, si cela ressemble à nodejs, les points d'arrêt peuvent être très bogués.
David Knipe