Deklaracja funkcji JavaScript i kolejność oceny

80

Dlaczego pierwszy z tych przykładów nie działa, a wszystkie inne działają?

// 1 - does not work
(function() {
setTimeout(someFunction1, 10);
var someFunction1 = function() { alert('here1'); };
})();

// 2
(function() {
setTimeout(someFunction2, 10);
function someFunction2() { alert('here2'); }
})();

// 3
(function() {
setTimeout(function() { someFunction3(); }, 10);
var someFunction3 = function() { alert('here3'); };
})();

// 4
(function() {
setTimeout(function() { someFunction4(); }, 10);
function someFunction4() { alert('here4'); }
})();
Wszyscy jesteśmy Monica
źródło

Odpowiedzi:

182

Nie jest to ani problem zakresu, ani nie jest to problem zamknięcia. Problem polega na zrozumieniu między deklaracjami a wyrażeniami .

Kod JavaScript, ponieważ nawet pierwsza wersja JavaScript firmy Netscape i pierwsza jej kopia Microsoftu jest przetwarzana w dwóch fazach:

Faza 1: kompilacja - w tej fazie kod jest kompilowany do postaci drzewa składni (i kodu bajtowego lub binarnego w zależności od silnika).

Faza 2: wykonanie - przeanalizowany kod jest następnie interpretowany.

Składnia deklaracji funkcji to:

function name (arguments) {code}

Argumenty są oczywiście opcjonalne (kod też jest opcjonalny, ale jaki to ma sens?).

Ale JavaScript umożliwia również tworzenie funkcji za pomocą wyrażeń . Składnia wyrażeń funkcji jest podobna do deklaracji funkcji, z wyjątkiem tego, że są one zapisywane w kontekście wyrażenia. A wyrażenia to:

  1. Cokolwiek na prawo od =znaku (lub :na literałach obiektu).
  2. Wszystko w nawiasach ().
  3. Parametry funkcji (w rzeczywistości jest to już objęte 2).

Wyrażenia w przeciwieństwie do deklaracji są przetwarzane w fazie wykonywania, a nie w fazie kompilacji. Z tego powodu kolejność wyrażeń ma znaczenie.

Tak więc, aby wyjaśnić:


// 1
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();

Faza 1: kompilacja. Kompilator widzi, że zmienna someFunctionjest zdefiniowana, więc ją tworzy. Domyślnie wszystkie utworzone zmienne mają wartość undefined. Należy zauważyć, że kompilator nie może jeszcze przypisać wartości w tym momencie, ponieważ wartości mogą wymagać, aby interpreter wykonał jakiś kod, aby zwrócić wartość do przypisania. Na tym etapie jeszcze nie wykonujemy kodu.

Faza 2: wykonanie. Interpreter widzi, że chcesz przekazać zmienną someFunctiondo setTimeout. I tak właśnie jest. Niestety aktualna wartość someFunctionjest nieokreślona.


// 2
(function() {
setTimeout(someFunction, 10);
function someFunction() { alert('here2'); }
})();

Faza 1: kompilacja. Kompilator widzi, że deklarujesz funkcję o nazwie someFunction i tworzy ją.

Faza 2: interpreter widzi, że chcesz przejść someFunctiondo setTimeout. I tak właśnie jest. Bieżąca wartość someFunctionjest deklaracją skompilowanej funkcji.


// 3
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();

Faza 1: kompilacja. Kompilator widzi, że zadeklarowałeś zmienną someFunctioni tworzy ją. Jak poprzednio, jego wartość jest nieokreślona.

Faza 2: wykonanie. Interpreter przekazuje anonimową funkcję do setTimeout w celu wykonania później. W tej funkcji widzi, że używasz zmiennej, someFunctionwięc tworzy zamknięcie zmiennej. W tym momencie wartość someFunctionjest nadal nieokreślona. Następnie widzi, jak przypisujesz funkcję do someFunction. W tym momencie wartość someFunctionnie jest już nieokreślona. 1/100 sekundy później wyzwala setTimeout i wywoływana jest funkcja someFunction. Ponieważ jego wartość nie jest już nieokreślona, ​​działa.


Przypadek 4 jest tak naprawdę inną wersją przypadku 2 z wrzuconym fragmentem przypadku 3. W punkcie someFunctionjest przekazywany do setTimeout, ponieważ już istnieje, ponieważ został zadeklarowany.


Dodatkowe wyjaśnienie:

Możesz się zastanawiać, dlaczego setTimeout(someFunction, 10)nie tworzy zamknięcia między lokalną kopią someFunction a przekazaną do setTimeout. Odpowiedzią na to jest to, że argumenty funkcji w JavaScript są zawsze, zawsze przekazywane przez wartość, jeśli są liczbami lub łańcuchami, lub przez odniesienie do wszystkiego innego. Tak więc setTimeout w rzeczywistości nie pobiera zmiennej someFunction przekazanej do niej (co oznaczałoby utworzenie domknięcia), ale raczej pobiera tylko obiekt, do którego odnosi się someFunction (który w tym przypadku jest funkcją). Jest to najczęściej używany mechanizm w JavaScript do łamania domknięć (na przykład w pętlach).

slebetman
źródło
7
To była naprawdę świetna odpowiedź.
Matt Briggs
1
@Matt: Wyjaśniłem to gdzie indziej (kilka razy) na SO. Niektóre z moich ulubionych wyjaśnień: stackoverflow.com/questions/3572480/ ...
slebetman
3
@Matt: Technicznie rzecz biorąc, zamknięcia obejmują nie zakres, ale ramkę stosu (znaną również jako rekord aktywacji). Zamknięcie to zmienna współdzielona między ramkami stosu. Ramka stosu ma określać zakres tego, czym obiekt jest dla klasy. Innymi słowy, zakres jest tym, co programista postrzega w strukturze kodu. Ramka stosu jest tworzona w pamięci w czasie wykonywania. Nie jest tak naprawdę, ale wystarczająco blisko. Myśląc o zachowaniu w czasie wykonywania, zrozumienie oparte na zakresie czasami nie wystarcza.
slebetman
3
@slebetman jako wyjaśnienie przykładu 3, wspomniałeś, że anonimowa funkcja w ramach setTimeout tworzy zamknięcie zmiennej someFunction i że w tym momencie someFunction jest nadal niezdefiniowana - co ma sens. Wygląda na to, że jedynym powodem, dla którego przykład 3 nie zwraca wartości undefined, jest funkcja setTimeout (opóźnienie wynoszące 10 milisekund umożliwia JavaScriptowi wykonanie następnej instrukcji przypisania do someFunction, a tym samym jej zdefiniowanie), prawda?
wmock
2

Zakres JavaScript jest oparty na funkcjach, a nie ściśle leksykalnie. oznacza to, że

  • Somefunction1 jest zdefiniowany od początku otaczającej funkcji, ale jej zawartość jest niezdefiniowana, dopóki nie zostanie przypisana.

  • w drugim przykładzie przypisanie jest częścią deklaracji, więc „przesuwa się” na górę.

  • w trzecim przykładzie zmienna istnieje, gdy zdefiniowano anonimowe zamknięcie wewnętrzne, ale jest używana dopiero 10 sekund później, wtedy wartość została przypisana.

  • czwarty przykład ma zarówno drugi, jak i trzeci powód, dla którego warto działać

Javier
źródło
1

Ponieważ someFunction1nie został jeszcze przypisany w momencie wykonywania wywołania setTimeout().

someFunction3 może wyglądać podobnej sprawie, ale ponieważ jesteś przechodzącą zawijanie funkcyjny someFunction3()aby setTimeout()w tym przypadku wywołanie someFunction3()nie jest oceniany dopiero później.

matowe b
źródło
Ale someFunction2czy nie został jeszcze przypisany, kiedy wywołanie setTimeout()jest wykonywane albo ...?
Wszyscy jesteśmy Monica
1
@jnylen: Zadeklarowanie funkcji za pomocą functionsłowa kluczowego nie jest dokładnie równoważne z przypisaniem funkcji anonimowej do zmiennej. Funkcje zadeklarowane jako function foo()są „podnoszone” na początek bieżącego zakresu, podczas gdy przypisania zmiennych następują w miejscu, w którym są zapisywane.
Chuck
1
+1 za funkcje specjalne. Jednak to, że może działać, nie oznacza, że ​​należy to zrobić. Zawsze deklaruj przed użyciem.
mway
@mway: w moim przypadku zorganizowałem swój kod w ramach „klasy” w sekcje: zmienne prywatne, programy obsługi zdarzeń, funkcje prywatne, a następnie funkcje publiczne. Potrzebuję jednego z moich programów obsługi zdarzeń, aby wywołać jedną z moich funkcji prywatnych. Dla mnie takie uporządkowanie kodu wygrywa nad leksykalnym porządkowaniem deklaracji.
Wszyscy jesteśmy Monica
1

To brzmi jak podstawowy przypadek przestrzegania dobrej procedury, aby uniknąć kłopotów. Zadeklaruj zmienne i funkcje przed ich użyciem i zadeklaruj funkcje takie jak:

function name (arguments) {code}

Unikaj deklarowania ich za pomocą var. To jest po prostu niechlujne i prowadzi do problemów. Jeśli wpadniesz w nawyk deklarowania wszystkiego przed użyciem, większość problemów zniknie w wielkim pośpiechu. Deklarując zmienne, inicjowałbym je od razu prawidłową wartością, aby upewnić się, że żadna z nich nie jest niezdefiniowana. Mam również tendencję do dołączania kodu, który sprawdza prawidłowe wartości zmiennych globalnych, zanim funkcja ich użyje. Jest to dodatkowe zabezpieczenie przed błędami.

Szczegóły techniczne tego wszystkiego przypominają fizykę działania granatu ręcznego, kiedy się nim bawisz. Moja prosta rada to przede wszystkim nie bawić się granatami ręcznymi.

Niektóre proste deklaracje na początku kodu mogą rozwiązać większość tego rodzaju problemów, ale nadal może być konieczne pewne uporządkowanie kodu.

Dodatkowa uwaga:
Przeprowadziłem kilka eksperymentów i wygląda na to, że jeśli zadeklarujesz wszystkie swoje funkcje w sposób opisany tutaj, nie ma znaczenia, w jakiej kolejności się one znajdują. Jeśli funkcja A używa funkcji B, funkcja B nie musi być zadeklarowane przed funkcją A.

Dlatego najpierw zadeklaruj wszystkie funkcje, następnie zmienne globalne, a drugi kod umieść na końcu. Postępuj zgodnie z tymi praktycznymi zasadami, a nie popełnisz błędu. Najlepiej byłoby nawet umieścić swoje deklaracje w nagłówku strony internetowej, a inny kod w treści, aby zapewnić egzekwowanie tych zasad.

Terry Prothero
źródło