Por que o depurador do Chrome acha que a variável local fechada é indefinida?

167

Com este código:

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

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

Eu recebo este resultado inesperado:

insira a descrição da imagem aqui

Quando eu mudo o código:

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

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

Eu recebo o resultado esperado:

insira a descrição da imagem aqui

Além disso, se houver alguma chamada para evaldentro da função interna, eu posso acessar minha variável como desejo (não importa para o que eu passo eval).

Enquanto isso, as ferramentas de desenvolvimento do Firefox fornecem o comportamento esperado nas duas circunstâncias.

O que há com o Chrome que o depurador se comporte menos do que o Firefox? Eu observo esse comportamento há algum tempo, incluindo a versão 41.0.2272.43 beta (64 bits).

Será que o mecanismo javascript do Chrome "nivela" as funções quando pode?

Curiosamente, se eu adicionar uma segunda variável que é referenciada na função interna, a xvariável ainda está indefinida.

Entendo que muitas vezes existem peculiaridades com escopo e definição de variável ao usar um depurador interativo, mas parece-me que, com base na especificação da linguagem, deve haver uma "melhor" solução para essas peculiaridades. Portanto, estou muito curioso se isso se deve à otimização do Chrome para além do Firefox. E também se essas otimizações podem ou não ser facilmente desabilitadas durante o desenvolvimento (talvez elas devam ser desabilitadas quando as ferramentas de desenvolvimento estão abertas?).

Além disso, eu posso reproduzir isso com pontos de interrupção e com a debuggerdeclaração.

Gabe Kopley
fonte
2
talvez ele está ficando variáveis-un utilizado fora do seu caminho para você ...
dandavis
markle976 parece estar dizendo que a debugger;linha não é realmente chamada de dentro bar. Portanto, observe o rastreamento de pilha quando ele pausa no depurador: a barfunção é mencionada no stacktrace? Se eu estiver certo, o stacktrace deve dizer que está pausado na linha 5, na linha 7, na linha 9.
David Knipe
Acho que não tem nada a ver com as funções de nivelamento da V8. Eu acho que isso é apenas uma peculiaridade; Eu não sei se eu chamaria isso de bug. Acho que a resposta de David abaixo faz mais sentido.
markle976
2
Eu tenho o mesmo problema, eu odeio isso. Mas quando eu preciso ter entradas de fechamento de acesso no console, vou para onde você pode ver o escopo, localizo a entrada Closure e a abro. Em seguida, clique com o botão direito do mouse no elemento necessário e clique em Armazenar como variável global . Uma nova variável global temp1é anexada ao console e você pode usá-la para acessar a entrada do escopo.
19417 Pablo

Respostas:

149

Encontrei um relatório de problema da v8 que é exatamente sobre o que você está perguntando.

Agora, para resumir o que é dito nesse relatório de problema ... a v8 pode armazenar as variáveis ​​locais para uma função na pilha ou em um objeto de "contexto" que fica na pilha. Ele alocará variáveis ​​locais na pilha, desde que a função não contenha nenhuma função interna que se refira a elas. É uma otimização . Se qualquer função interna se referir a uma variável local, essa variável será colocada em um objeto de contexto (ou seja, na pilha em vez de na pilha). O caso de evalé especial: se é chamado de alguma forma por uma função interna, todas as variáveis ​​locais são colocadas no objeto de contexto.

A razão para o objeto de contexto é que, em geral, você pode retornar uma função interna da externa e, em seguida, a pilha que existia enquanto a função externa era executada não estará mais disponível. Portanto, qualquer coisa acessada pela função interna precisa sobreviver à função externa e viver na pilha, e não na pilha.

O depurador não pode inspecionar as variáveis ​​que estão na pilha. Com relação ao problema encontrado na depuração, um Membro do Projeto diz :

A única solução em que pude pensar é que sempre que o devtools estiver ativado, desoptamos todo o código e recompilamos com a alocação forçada de contexto. Isso regrediria drasticamente o desempenho com os devtools ativados.

Aqui está um exemplo do "se alguma função interna se refere à variável, coloque-a em um objeto de contexto". Se você executar isso, poderá acessar xa debuggerinstrução mesmo que xseja usada apenas na foofunção, que nunca é chamada !

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();
Louis
fonte
13
Você descobriu uma maneira de adotar código? Eu gosto de usar o depurador como um REPL e o código lá e depois transferir o código para meus próprios arquivos. Mas muitas vezes não é viável, pois as variáveis ​​que deveriam estar lá não são acessíveis. Uma avaliação simples não serve. Eu ouço uma força infinita para loop.
Raio Foss
Na verdade, não deparei com esse problema durante a depuração, por isso não procurei maneiras de desativar o código.
Louis
6
O último comentário da questão diz: É possível colocar a V8 em um modo em que tudo é alocado à força pelo contexto, mas não sei como / quando acioná-lo por meio da interface do usuário do Devtools. Para fins de depuração, algumas vezes eu gostaria . Como posso forçar esse modo?
Suma
2
@ user208769 Ao fechar como duplicado, favorecemos a pergunta que é mais útil para futuros leitores. Existem vários fatores que ajudam a determinar qual pergunta é mais útil: sua pergunta obteve exatamente 0 respostas, enquanto essa obteve várias respostas votadas. Portanto, esta pergunta é a mais útil das duas. As datas se tornam um fator determinante apenas se a utilidade for maioritariamente igual.
Louis
1
Esta resposta responde à pergunta real (Por quê?), Mas à pergunta implícita - Como obtenho acesso a variáveis ​​de contexto não utilizadas para depuração sem adicionar referências extras a elas no meu código? - é melhor respondida por @OwnageIsMagic abaixo.
Sigfried 27/07
30

Como o @Louis disse, causado por otimizações da v8. Você pode atravessar a pilha de chamadas para o quadro em que essa variável está visível:

call1 call2

Ou substitua debuggerpor

eval('debugger');

eval vai adiar pedaço atual

OwnageIsMagic
fonte
1
Quase ótimo! Ele pausa em um módulo de VM (amarelo) com conteúdo debuggere o contexto está realmente disponível. Se você aumentar a pilha um nível para o código que está realmente tentando depurar, voltará a não ter acesso ao contexto. Portanto, é um pouco desajeitado, não sendo possível ver o código que você está depurando enquanto acessa variáveis ​​de fechamento ocultas. Eu vou votar, no entanto, uma vez que me impede de adicionar código que não é obviamente para depuração, e me dá acesso a todo o contexto sem desativar o aplicativo todo.
Sigfried 27/07
Ah ... é ainda mais desajeitado do que ter que usar a evaljanela de código-fonte amarela para obter acesso ao contexto: você não pode percorrer o código (a menos que coloque eval('debugger')entre todas as linhas que deseja percorrer.)
Sigfried
Parece que há situações em que determinadas variáveis ​​são invisíveis, mesmo depois de passar para o quadro de pilha apropriado; Eu tenho algo parecido controllers.forEach(c => c.update())e atingi um ponto de interrupção em algum lugar no fundo c.update(). Se eu selecionar o quadro em que controllers.forEach()é chamado, controllersserá indefinido (mas tudo o mais nesse quadro estará visível). Não consegui reproduzir com uma versão mínima, suponho que possa haver algum limite de complexidade que precise ser passado ou algo assim.
precisa saber é o seguinte
@PeterT se é <indefinido> você está no lugar errado ou somewhere deep inside c.update()o seu código vai assíncrona e você vê quadro de pilha assíncrona
OwnageIsMagic
6

Eu também notei isso no nodejs. Acredito (e admito que isso é apenas um palpite) que, quando o código é compilado, se xnão aparecer dentro bar, ele não fica xdisponível dentro do escopo de bar. Isso provavelmente o torna um pouco mais eficiente; o problema é que alguém esqueceu (ou não se importava) que, mesmo se não há é xem bar, você pode optar por executar o depurador e, portanto, ainda precisa de acesso xa partir de dentro bar.

David Knipe
fonte
2
Obrigado. Basicamente, eu quero poder explicar isso para iniciantes em javascript melhor do que "O depurador está".
Gabe Kopley
@GabeKopley: Tecnicamente, o depurador não está mentindo. Se uma variável não é referenciada, ela não é tecnicamente fechada. Portanto, não é necessário que o intérprete crie o fechamento.
slebetman
7
Essa não é a questão. Ao usar o depurador, frequentemente estive em uma situação em que queria saber o valor de uma variável em um escopo externo, mas não consegui por causa disso. E em uma nota mais filosófica, eu diria que o depurador está mentindo. Se a variável existe no escopo interno não deve depender se é realmente usada ou se existe um evalcomando não relacionado . Se a variável for declarada, ela deverá estar acessível.
David Knipe
2

Uau, muito interessante!

Como outros já mencionaram, isso parece estar relacionado scope, mas mais especificamente, a debugger scope. Quando o script injetado é avaliado nas ferramentas do desenvolvedor, parece determinar a ScopeChain, o que resulta em alguma peculiaridade (já que está vinculado ao escopo do inspetor / depurador). Uma variação do que você postou é o seguinte:

(EDIT - na verdade, você mencionou isso na sua pergunta original, caramba, meu mal! )

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

Para os ambiciosos e / ou curiosos, escopo (heh) a fonte para ver o que está acontecendo:

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

Jack
fonte
0

Suspeito que isso tenha a ver com o levantamento de variáveis ​​e funções. JavaScript traz todas as declarações de variáveis ​​e funções para o topo da função em que estão definidas. Mais informações aqui: http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/

Aposto que o Chrome está chamando o ponto de interrupção com a variável indisponível para o escopo, porque não há mais nada na função. Isso parece funcionar:

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

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

Como faz isso:

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

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

Espero que isso e / ou o link acima ajude. Este é o meu tipo favorito de perguntas sobre SO, BTW :)

markle976
fonte
Obrigado! :) Eu estou querendo saber o que FF faz diferente. Da minha perspectiva como um desenvolvedor, a experiência FF é objetivamente melhor ...
Gabe Kopley
2
"chamando o ponto de ruptura no horário do lex" duvido. Não é para isso que servem os pontos de interrupção. E não vejo por que a ausência de outras coisas na função deve importar. Dito isto, se é algo como nodejs, os pontos de interrupção podem ser muito problemáticos.
David Knipe