Dlaczego debuger Chrome uważa, że ​​zamknięta zmienna lokalna jest niezdefiniowana?

167

Za pomocą tego kodu:

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

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

Otrzymuję ten nieoczekiwany wynik:

wprowadź opis obrazu tutaj

Kiedy zmieniam kod:

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

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

Otrzymuję oczekiwany wynik:

wprowadź opis obrazu tutaj

Ponadto, jeśli jest jakieś wywołanie evalw funkcji wewnętrznej, mogę uzyskać dostęp do mojej zmiennej tak, jak chcę (nie ma znaczenia, do czego przekażę eval).

W międzyczasie narzędzia deweloperskie Firefoksa zapewniają oczekiwane zachowanie w obu przypadkach.

Co jest z Chrome, że debugger zachowuje się mniej wygodnie niż Firefox? Obserwowałem to zachowanie od jakiegoś czasu, aż do wersji beta 41.0.2272.43 włącznie (wersja 64-bitowa).

Czy to dlatego, że silnik javascript Chrome „spłaszcza” funkcje, kiedy jest to możliwe?

Co ciekawe, jeśli dodam drugą zmienną, która jest odwołanie w funkcji wewnętrznej,x zmienna jest nadal nieokreślone.

Rozumiem, że podczas korzystania z interaktywnego debuggera często pojawiają się dziwactwa związane z zakresem i definicją zmiennych, ale wydaje mi się, że w oparciu o specyfikację języka powinno być „najlepsze” rozwiązanie tych dziwactw. Jestem więc bardzo ciekawy, czy wynika to z dalszej optymalizacji Chrome niż Firefox. A także, czy te optymalizacje można łatwo wyłączyć podczas programowania (może powinny być wyłączone, gdy narzędzia deweloperskie są otwarte?).

Mogę również odtworzyć to z punktami przerwania, a także z debuggerinstrukcją.

Gabe Kopley
źródło
2
może
sprawia
markle976 wydaje się mówić, że debugger;linia nie jest wywoływana od wewnątrz bar. Spójrz więc na ślad stosu, gdy zatrzymuje się w debugerze: Czy barfunkcja jest wymieniona w stosie śledzenia? Jeśli mam rację, to ślad stosu powinien wskazywać, że jest wstrzymany w linii 5, w linii 7, w linii 9.
David Knipe,
Nie sądzę, że ma to coś wspólnego z funkcjami spłaszczania V8. Myślę, że to tylko dziwactwo; Nie wiem, czy w ogóle nazwałbym to błędem. Myślę, że odpowiedź Davida poniżej jest najbardziej sensowna.
markle976,
2
Mam ten sam problem, nienawidzę tego. Ale kiedy potrzebuję uzyskać dostęp do wpisów zamknięcia w konsoli, przechodzę do miejsca, w którym można zobaczyć zakres, znaleźć wpis Zamknięcie i otworzyć go. Następnie kliknij prawym przyciskiem myszy potrzebny element i kliknij Zapisz jako zmienną globalną . temp1Do konsoli dołączona jest nowa zmienna globalna, której można użyć, aby uzyskać dostęp do wpisu zakresu.
Pablo

Odpowiedzi:

149

Znalazłem raport o problemie z wersją 8, który dokładnie dotyczy tego, o co pytasz.

Teraz, podsumowując to, co zostało powiedziane w tym raporcie o problemie ... v8 może przechowywać zmienne, które są lokalne dla funkcji na stosie lub w obiekcie „kontekstu”, który żyje na stercie. Przydzieli zmienne lokalne na stosie, o ile funkcja nie zawiera żadnej funkcji wewnętrznej, która się do nich odwołuje. To jest optymalizacja . Jeśli jakakolwiek funkcja wewnętrzna odwołuje się do zmiennej lokalnej, zmienna ta zostanie umieszczona w obiekcie kontekstu (tj. Na stercie zamiast na stosie). Przypadek evaljest szczególny: jeśli w ogóle jest wywoływana przez funkcję wewnętrzną, wszystkie zmienne lokalne są umieszczane w obiekcie kontekstu.

Przyczyną istnienia obiektu kontekstu jest to, że generalnie można zwrócić funkcję wewnętrzną z funkcji zewnętrznej, a wtedy stos, który istniał, podczas gdy uruchomiona funkcja zewnętrzna, nie będzie już dostępny. Zatem wszystko, do czego funkcja wewnętrzna uzyskuje dostęp, musi przetrwać funkcję zewnętrzną i żyć na stercie, a nie na stosie.

Debuger nie może sprawdzić tych zmiennych, które znajdują się na stosie. Odnośnie problemu napotkanego podczas debugowania, jeden z członków projektu mówi :

Jedynym rozwiązaniem, jakie przychodzi mi do głowy, jest to, że za każdym razem, gdy devtools jest włączone, dezoptujemy cały kod i rekompilujemy z wymuszonym przydzieleniem kontekstu. To jednak dramatycznie zmniejszyłoby wydajność przy włączonych narzędziach Devtools.

Oto przykład „jeśli jakakolwiek funkcja wewnętrzna odwołuje się do zmiennej, umieść ją w obiekcie kontekstu”. Jeśli to uruchomisz, będziesz mieć dostęp xdo debuggerinstrukcji, nawet jeśli xjest ona używana tylko w foofunkcji, która nigdy nie jest wywoływana !

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

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

  function bar() {
    debugger;
  };

  bar();
}
baz();
Louis
źródło
13
Czy znalazłeś sposób na dezaktywację kodu? Lubię używać debugera jako REPL i koduję tam, a następnie przesyłam kod do moich własnych plików. Ale często jest to niewykonalne, ponieważ zmienne, które powinny tam być, są niedostępne. Zwykła ewaluacja tego nie zrobi. Słyszę nieskończoną moc pętli for.
Ray Foss
Właściwie nie napotkałem tego problemu podczas debugowania, więc nie szukałem sposobów na dezaktywację kodu.
Louis
6
Ostatni komentarz z numeru mówi: Przełączenie V8 w tryb, w którym wszystko jest na siłę przydzielone kontekstowo, jest możliwe, ale nie jestem pewien, jak / kiedy wywołać to przez interfejs Devtools Ze względu na debugowanie czasami chciałbym to zrobić . Jak mogę wymusić taki tryb?
Suma
2
@ user208769 Zamykając jako duplikat, preferujemy pytanie, które jest najbardziej przydatne dla przyszłych czytelników. Istnieje wiele czynników, które pomagają określić, które pytanie jest najbardziej przydatne: Twoje pytanie otrzymało dokładnie 0 odpowiedzi, podczas gdy na to pytanie zostało udzielonych wiele odpowiedzi za. Więc to pytanie jest najbardziej przydatne z dwóch. Daty stają się decydującym czynnikiem tylko wtedy, gdy użyteczność jest w większości równa.
Louis
1
Ta odpowiedź odpowiada na rzeczywiste pytanie (dlaczego?), Ale na pytanie domniemane - Jak uzyskać dostęp do nieużywanych zmiennych kontekstowych w celu debugowania bez dodawania do nich dodatkowych odwołań w moim kodzie? - lepiej odpowiada @OwnageIsMagic poniżej.
Sigfried
30

Jak @Louis powiedział, że jest to spowodowane optymalizacjami w wersji 8. Możesz przechodzić ze stosu wywołań do ramki, w której ta zmienna jest widoczna:

zadzwoń1 zadzwoń2

Lub wymienić debuggerz

eval('debugger');

eval deoptuje bieżący fragment

OwnageIsMagic
źródło
1
Prawie świetnie! Zatrzymuje się w module VM (żółtym) z zawartością debuggeri kontekst jest rzeczywiście dostępny. Jeśli zwiększysz stos o jeden poziom do kodu, który faktycznie próbujesz debugować, wrócisz do braku dostępu do kontekstu. Jest to więc po prostu trochę niezgrabne, ponieważ nie można spojrzeć na debugowany kod podczas uzyskiwania dostępu do ukrytych zmiennych zamknięcia. Głosuję jednak za, ponieważ oszczędza mi to konieczności dodawania kodu, który nie jest oczywiście do debugowania, i daje mi dostęp do całego kontekstu bez dezoptymalizacji całej aplikacji.
Sigfried
Och ... to nawet trudniejsze niż użycie żółtego evalokna źródłowego ed, aby uzyskać dostęp do kontekstu: nie możesz przechodzić przez kod (chyba że umieścisz eval('debugger')między wszystkimi liniami, przez które chcesz przejść).
Sigfried
Wydaje się, że zdarzają się sytuacje, w których pewne zmienne są niewidoczne nawet po przejściu do odpowiedniej ramki stosu; Mam coś podobnego controllers.forEach(c => c.update())i trafiłem gdzieś głęboko w breakpoint c.update(). Jeśli następnie zaznaczę ramkę, w której controllers.forEach()jest wywołana, controllersjest niezdefiniowana (ale wszystko inne w tej ramce jest widoczne). Nie mogłem odtworzyć w wersji minimalnej, przypuszczam, że może być jakiś próg złożoności, który należy przejść lub coś w tym stylu.
PeterT
@PeterT, jeśli jest <undefined>, to jesteś w złym miejscu Lub somewhere deep inside c.update()Twój kod przechodzi w stan asynchroniczny i widzisz ramkę stosu asynchronicznego
OwnageIsMagic
6

Zauważyłem to również w nodejs. Uważam (i przyznaję, że to tylko przypuszczenie), że gdy kod jest kompilowany, jeśli xnie pojawia się w środku bar, nie jest xdostępny w zakresie bar. To prawdopodobnie czyni go nieco bardziej wydajnym; problem jest ktoś zapomniał (lub nie obchodzi), że nawet jeśli nie znajduje się xw bar, można zdecydować, aby uruchomić debugger, a więc nadal mieć dostęp xod wewnątrz bar.

David Knipe
źródło
2
Dzięki. Zasadniczo chcę być w stanie wyjaśnić to początkującym w javascript lepiej niż „Debugger kłamie”.
Gabe Kopley
@GabeKopley: Technicznie rzecz biorąc, debugger nie kłamie. Jeśli zmienna nie jest przywoływana, to nie jest technicznie ujęta. Nie ma więc potrzeby, aby tłumacz ustny tworzył zamknięcie.
slebetman
7
Nie o to chodzi. Podczas korzystania z debuggera często znajdowałem się w sytuacji, w której chciałem poznać wartość zmiennej w zakresie zewnętrznym, ale nie mogłem z tego powodu. A z bardziej filozoficznego punktu widzenia, powiedziałbym, że debugger kłamie. To, czy zmienna istnieje w zakresie wewnętrznym, nie powinno zależeć od tego, czy jest faktycznie używana, czy też istnieje niepowiązane evalpolecenie. Jeśli zmienna jest zadeklarowana, powinna być dostępna.
David Knipe,
2

Wow, naprawdę interesujące!

Jak wspominali inni, wydaje się, że jest to związane scope, ale dokładniej, związane z debugger scope. Kiedy wstrzykiwany skrypt jest oceniany w narzędziach programistycznych, wydaje się, że określa plikScopeChain , co powoduje pewne dziwactwa (ponieważ jest powiązany z zakresem inspektora / debugera). Oto odmiana tego, co opublikowałeś:

(EDYTUJ - właściwie wspomniałeś o tym w swoim pierwotnym pytaniu , o mój Boże! )

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

Dla ambitnych i / lub ciekawskich, sprawdź (heh) źródło, aby zobaczyć, co się dzieje:

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

Jacek
źródło
0

Podejrzewam, że ma to związek z podnoszeniem zmiennym i funkcyjnym. JavaScript przenosi wszystkie deklaracje zmiennych i funkcji na początek funkcji, w której są zdefiniowane. Więcej informacji tutaj: http://jamesallardice.com/explaining-function-and-variable-hoisting-in-javascript/

Założę się, że Chrome wywołuje punkt przerwania ze zmienną niedostępną dla zakresu, ponieważ w funkcji nie ma nic innego. To wydaje się działać:

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

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

Jak to robi:

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

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

Mam nadzieję, że to i / lub powyższy link pomoże. To są moje ulubione rodzaje pytań SO, przy okazji :)

markle976
źródło
Dzięki! :) Zastanawiam się, co robi FF inaczej. Z mojej perspektywy jako programisty, doświadczenie FF jest obiektywnie lepsze ...
Gabe Kopley
2
„wzywając punkt przerwania w czasie lex” Wątpię. Nie do tego służą punkty przerwania. I nie rozumiem, dlaczego brak innych rzeczy w funkcji miałby mieć znaczenie. Powiedziawszy to, jeśli jest to coś w rodzaju nodejs, punkty przerwania mogą być bardzo błędne.
David Knipe,