100930746

Page 1


Rozdział 3.

Łańcuch zakresów

W rozdziałach 1 i 2 przedstawiliśmy konkretną definicję zakresu leksykalnego (i jego części) oraz zilustrowaliśmy metafory pomagające w zrozumieniu podstaw konceptualnych. Zanim przejdziesz do dalszej części tego rozdziału, znajdź kogoś, komu możesz wytłumaczyć własnymi słowami (pisemnie lub ustnie), czym jest zakres leksykalny i dlaczego warto go zrozumieć.

Może pojawić się pokusa, by pominąć ten krok, ale naszym zdaniem naprawdę warto poświęcić czas na przemyślenie idei jako wyjaśnienia dla innych osób. W ten sposób pomagamy naszym mózgom przetrawić to, czego się uczymy!

Czas zagłębić się w szczegóły, dlatego w dalszej części spodziewaj się dużo więcej detali. Jednak nie zniechęcaj się, ponieważ te omówienia naprawdę pokazują, jak mało wiemy o zakresach. Przeznacz odpowiednią ilość czasu na przestudiowanie tego tekstu i wszystkich przedstawionych fragmentów kodu.

Aby odświeżyć kontekst naszego przykładu, przypomnijmy ilustrację zagnieżdżonych zakresów oznaczonych za pomocą kolorowych ramek (rysunek 2 w rozdziale 2):

Rysunek 2 (rozdział 2). Kolorowe ramki zakresów

Połączenia między zakresami, które są zagnieżdżone w innych zakresach, nazywamy łańcuchem zakresów (scope chain). Łańcuch zakresów określa ścieżkę, zgodnie z którą można uzyskiwać dostęp do zmiennych. Ten łańcuch jest skierowany, co oznacza, że wyszukiwanie można przeprowadzać tylko w górę/na zewnętrz.

„Wyszukiwanie” jest (głównie) konceptualne

Zwróć uwagę na kolor odwołania do zmiennej students w pętli for na rysunku 2. Skąd wiadomo, że jest ono kulką CZERWONY(1)?

W rozdziale 2 opisaliśmy uzyskiwanie dostępu do zmiennej jako „wyszukiwanie”, które Silnik rozpoczyna od spytania Menedżera bieżącego zakresu, czy słyszał o identyfikatorze/zmiennej. A następnie wraca w górę/na zewnątrz przez łańcuch zagnieżdżonych zakresów (w kierunku zakresu globalnego), aż (potencjalnie) znajdzie deklarację. Wyszukiwanie zostaje zatrzymane, gdy odnaleziona zostanie pierwsza pasująca nazwana deklaracja w kubełku zakresu.

W procesie wyszukiwania ustaliśmy, że students jest kulką CZERWONY(1), ponieważ przechodząc po łańcuchu zakresów nie znaleźliśmy pasującej nazwy zmiennej, dopóki nie dotarliśmy do zakresu globalnego CZERWONY(1).

Analogicznie ustaliliśmy, że studentID w instrukcji if to kulka NIEBIESKI(2).

Ten opis procesu wyszukiwania w czasie uruchomienia pomaga w zrozumieniu ogólnej koncepcji, ale w praktyce sytuacja wygląda zwykle nieco inaczej.

Rozdział 3. Łańcuch zakresów

Kolor kubełka kulki (czyli metainformacja o tym, z którego zakresu pochodzi dana zmienna) jest zwykle ustalany w procesie kompilacji w trakcie wstępnego przetwarzania. Ponieważ zakres leksykalny jest w tym momencie prawie ukończony, nic, co zachodzi później w czasie uruchomienia, nie zmienia koloru kulki. Ponieważ kolor kulki jest znany od czasu kompilacji i nie zmienia się, ta informacja jest najprawdopodobniej przechowywana we wpisie każdej zmiennej w drzewie AST (a przynajmniej jest tam dostępna). Ta informacja jest następnie jawnie wykorzystywana przez instrukcje wykonywalne realizowane w czasie uruchomienia programu.

Innymi słowy, Silnik (patrz s. 41 w rozdziale 2) nie musi przeszukiwać wielu zakresów w celu sprawdzenia, z którego kubełka zakresu pochodzi dana zmienna. Ta informacja jest już znana! Wyeliminowanie konieczności wyszukiwania w czasie wykonania programu jest kluczową zaletą zakresu leksykalnego. Wszystko przebiega wydajniej, gdy nie trzeba poświęcać czasu na te wszystkie wyszukania. Opisując przed chwilą proces ustalania koloru kulki w czasie kompilacji, użyłem sformułowania „...zwykle ustalany...”. Czy może się zatem zdarzyć, że nie będzie on znany w czasie kompilacji?

Weźmy pod uwagę odwołanie do zmiennej, która nie jest zadeklarowana w żadnym leksykalnie dostępnym zakresie w aktualnym pliku – w rozdziale 1 książki Na dobry początek pokazujemy, że z perspektywy kompilacji JS każdy plik jest osobnym programem. Jeśli żadna deklaracja nie zostanie znaleziona, niekoniecznie jest to błąd. Niewykluczone, że w czasie uruchomienia inny plik (program) deklaruje tę zmienną we wspólnym zakresie globalnym.

Dlatego ostateczne ustalenie, czy zmienna została prawidłowo zadeklarowana w jakimś dostępnym kubełku, trzeba będzie odroczyć do czasu wykonania.

Wszelkie odwołania do zmiennych, które są wstępnie niezadeklarowane pozostają niepokolorowanymi kulkami w czasie kompilacji pliku. Ich koloru nie można ustalić do czasu skompilowania odpowiedniego innego pliku (-ów) i uruchomienia aplikacji. To odroczone wyszukiwanie pozwoli w końcu ustalić, w jakim zakresie znaleźć można zmienną (najprawdopodobniej w zakresie globalnym).

Jednak to wyszukanie trzeba byłoby przeprowadzić maksymalnie jeden raz dla zmiennej, ponieważ później w czasie wykonania nic innego nie może zmienić koloru kulki.

W punkcie „Niepowodzenia wyszukiwania” w rozdziale 2 wyjaśniliśmy, co dzieje się, gdy kulka pozostaje niepokolorowana, dopóki jej odwołanie nie zostaje wykonane w czasie uruchomienia.

„Wyszukiwanie” jest (głównie) konceptualne

Podsumowując omówione do tej pory środowiska JS, program może lub też nie:

• Zadeklarować zmienną globalną w zakresie najwyższego poziomu przy użyciu deklaracji var lub function – bądź let, const i class.

• Dodać również deklaracje zmiennych globalnych jako właściwości obiektu zakresu globalnego, jeśli w deklaracji użyto var lub function.

• Odwołać się do obiektu zakresu globalnego (w celu dodania lub pobrania zmiennych globalnych jako właściwości) przy użyciu window, self lub global.

Moim zdaniem, wielu programistów nie docenia złożoności problemu dostępu i działania zakresu globalnego, co zostało zilustrowane w dotychczasowej części rozdziału. Jednak ta złożoność staje się najbardziej oczywista, gdy próbujemy ustalić uniwersalne odwołanie do obiektu zakresu globalnego.

Kolejna „sztuczka” umożliwiająca uzyskanie odwołania do obiektu zakresu globalnego wygląda następująco:

const theGlobalScopeObject = (new Function("return this"))();

Uwaga

Na podstawie kodu przechowywanego w wartości łańcucha można dynamicznie skonstruować funkcję, korzystając z konstruktora Function() , podobnie do eval(..) (patrz „Oszukiwanie: zmienianie zakresu w czasie wykonania” w rozdziale 1). Taka funkcja zostanie automatycznie uruchomiona w trybie nieścisłym (ze względu na starsze wersje), gdy wywołamy ją przy użyciu normalnego wywołania funkcji (), jak w tym przykładzie. Jej this będzie wskazywać obiekt globalny. Dodatkowe informacje o określaniu wiązań this znaleźć można w trzeciej książce z serii zatytułowanej Objects & Classes1

A zatem mamy do dyspozycji window, self, global i brzydką sztuczkę new Function(..). To wiele różnych metod uzyskiwania dostępu do obiektu zakresu globalnego. Każda z nich ma pewne wady i zalety.

Czemu nie wprowadzić jeszcze jednej metody!?

1 Książka w trakcie opracowywania (przyp. red.).

W wersji ES2020 wreszcie zdefiniowano ustandaryzowane odwołanie do obiektu zakresu globalnego o nazwie globalThis. Dlatego w zależności od wersji silnika JS, w jakim uruchamiany jest kod, można użyć globalThis zamiast dowolnej z tych metod.

Moglibyśmy nawet spróbować zdefiniować polyfill dla różnych środowisk, który jest bezpieczniejszy w różnych środowiskach JS poprzedzających globalThis, taki jak:

const theGlobalScopeObject = (typeof globalThis != "undefined") ? globalThis : (typeof global != "undefined") ? global : (typeof window != "undefined") ? window : (typeof self != "undefined") ? self : (new Function("return this"))();

Uff! Z pewnością nie jest to idealne rozwiązanie, ale może się przydać, gdy będziesz potrzebować rzetelnego odwołania do zakresu globalnego.

Tak na marginesie, proponowana nazwa globalThis wzbudziła pewne kontrowersje, gdy funkcja była dodawana do JS. A dokładniej, ja i wiele innych osób uważaliśmy, że użycie „this” w jej nazwie jest mylące, ponieważ odwołanie do tego obiektu miało służyć do uzyskiwania dostępu do zakresu globalnego, a nie jakiegoś globalnego/domyślnego wiązania this. Pod uwagę brano wiele innych nazw, ale z różnych przyczyn zostały one wykluczone. Niestety wybrana została nazwa, która była traktowana jako ostateczność. Jeśli planujesz wykorzystywać w swoich programach obiekt zakresu globalnego, zdecydowanie zalecam wybranie lepszej nazwy, takiej jak (groteskowo długa, ale precyzyjna!) theGlobalScopeObject użyta w tym przykładzie.

Zrozumienie zakresu globalnego

Zakres globalny jest obecny i ważny w każdym programie JS, mimo iż nowoczesne wzorce organizacji kodu w moduły ograniczają konieczność przechowywania identyfikatorów w tej przestrzeni nazw.

Mimo to, w miarę jak nasz kod wychodzi coraz bardziej poza granice przeglądarki, szczególnie ważne jest dobre zrozumienie różnic w działaniu zakresu globalnego (i obiektu zakresu globalnego!) w różnych środowiskach JS.

Skoro mamy już wyraźniejszy ogólny obraz zakresu globalnego, w kolejnym rozdziale ponownie zagłębimy się w szczegóły zakresu leksykalnego, analizując, jak i kiedy można korzystać ze zmiennych.

Rozdział 4. Wokół zakresu globalnego

Typowe domknięcia: Ajax i zdarzenia

Domknięcia najczęściej spotykamy w wywołaniach zwrotnych:

function lookupStudentRecord(studentID) { ajax( 'https://some.api/student/${ studentID }' , function onRecord(record) { console.log( '${ record.name } (${ studentID })' ); } ); }

lookupStudentRecord(114); // Frank (114)

Wywołanie zwrotne onRecord(..) zostanie wywołane w przyszłości, gdy wróci odpowiedź z wywołania Ajax. To wywołanie zostanie zainicjalizowane z wnętrza narzędzia ajax(..), skądkolwiek ono pochodzi. Co więcej, nastąpi to długo po wykonaniu lookupStudentRecord(..).

Jak to możliwe, że zmienna studentID będzie nadal dostępna w wywołaniu zwrotnym? Odpowiedzią jest domknięcie.

Domknięcia są też często wykorzystywane do obsługi zdarzeń:

function listenForClicks(btn,label) { btn.addEventListener("click",function onClick(){ console.log( 'Przycisk ${ label } został kliknięty!' ); }); }

var submitBtn = document.getElementById("submit-btn"); listenForClicks(submitBtn,"Checkout");

Wywołanie zwrotne obsługi zdarzenia onClick(..) domyka się na parametrze label . Gdy przycisk zostanie kliknięty, zmienna label będzie nadal istniała i będzie można jej użyć. Jest to domknięcie.

Dodatek B. Ćwiczenia

Celem tego dodatku jest dostarczenie pewnych wymagających i interesujących zadań, które pozwolą ci przetestować i utrwalić znajomość głównych zagadnień omawianych w tej książce. Dobrze byłoby, gdybyś spróbował najpierw samodzielnie rozwiązać te problemy – w prawdziwym edytorze kodu! – zamiast od razu sprawdzać rozwiązania zamieszczone na końcu książki. Proszę nie ściągać!

Te ćwiczenia nie mają jednej prawidłowej odpowiedzi, na którą musisz wpaść. Twoje podejście może trochę (lub bardzo!) różnić się od przedstawionych rozwiązań i to dobrze.

Nikt nie będzie osądzał cię za to, jak napisałeś swój kod. Mam nadzieję, że dzięki tej książce zyskałeś pewność, że potrafisz poradzić sobie z tego rodzaju zadaniami programistycznymi, bazując na solidnej wiedzy. To jedyny mój cel. Jeśli ty jesteś zadowolony ze swojego kodu, to ja również!

Kubełki wypełnione kulkami

Pamiętasz rysunek 2 z rozdziału 2?

Rysunek 2 (rozdz. 2). Kolorowe ramki zakresów

To ćwiczenie polega na napisaniu programu (dowolnego programu!), który zawiera zagnieżdżone funkcje i zakresy bloku spełniające następujące wymagania:

• Gdy pokolorujesz wszystkie zakresy (łącznie z globalnym!) różnymi kolorami, potrzebujesz przynajmniej sześciu kolorów. Pamiętaj o dodaniu dla każdego zakresu komentarza oznaczającego jego kolor.

Dodatkowe zadanie: zidentyfikuj wszystkie zakresy implikowane w swoim programie.

• Każdy zakres zawiera przynajmniej jeden identyfikator.

• Program zawiera przynamniej dwa zakresy funkcji i przynajmniej dwa zakresy bloku.

• Przynajmniej jedna zmienna z zakresu zewnętrznego ma być przesłonięta przez zmienną z zagnieżdżonego zakresu (patrz s. 30 w rozdziale 3).

• Przynajmniej jedno odwołanie do zmiennej musi być rozpoznane jako deklaracja zmiennej znajdująca się przynajmniej dwa poziomy wyżej w łańcuchu zakresów.

Uwaga

W tym ćwiczeniu możesz napisać bezużyteczny kod typu foo/bar/ baz, ale proponuję wymyślić jakiś nietrywialny, nieco realistyczny kod, który robi coś sensownego.

Spróbuj rozwiązać ćwiczenie samodzielnie, a następnie sprawdź sugerowane rozwiązanie przedstawione na końcu tego dodatku.

Domknięcie (część 1)

Zacznijmy od przećwiczenia domknięć na pewnych popularnych działaniach matematyczno-informatycznych: sprawdzania, czy wartość jest liczbą pierwszą (dzieli się tylko przez 1 i samą siebie) oraz generowania listy czynników pierwszych (dzielników) danej liczby.

Na przykład:

isPrime(11); // true isPrime(12); // false

factorize(11); // [ 11 ]

factorize(12); // [ 3, 2, 2 ] --> 3*2*2=12

Dodatek B. Ćwiczenia

Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.