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:
- Cokolwiek na prawo od
=
znaku (lub :
na literałach obiektu).
- Wszystko w nawiasach
()
.
- 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ć:
(function() {
setTimeout(someFunction, 10);
var someFunction = function() { alert('here1'); };
})();
Faza 1: kompilacja. Kompilator widzi, że zmienna someFunction
jest 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ą someFunction
do setTimeout. I tak właśnie jest. Niestety aktualna wartość someFunction
jest nieokreślona.
(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ść someFunction
do setTimeout. I tak właśnie jest. Bieżąca wartość someFunction
jest deklaracją skompilowanej funkcji.
(function() {
setTimeout(function() { someFunction(); }, 10);
var someFunction = function() { alert('here3'); };
})();
Faza 1: kompilacja. Kompilator widzi, że zadeklarowałeś zmienną someFunction
i 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, someFunction
więc tworzy zamknięcie zmiennej. W tym momencie wartość someFunction
jest nadal nieokreślona. Następnie widzi, jak przypisujesz funkcję do someFunction
. W tym momencie wartość someFunction
nie 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 someFunction
jest 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).