Porządek w Javascript

Zakres jest jednym z podstawowych aspektów języka JavaScript i prawdopodobnie tym, z którym miałem najwięcej problemów przy tworzeniu złożonych programów. Nie mogę policzyć, ile razy straciłem kontrolę nad tym, do czego odnosi się to słowo kluczowe po przejściu kontroli od funkcji do funkcji, i często zdarzało mi się, że pisałem mój kod na różne mylące sposoby, próbując zachować pewne pozory zdrowego rozsądku w moim rozumieniu, które zmienne były dostępne gdzie.

W tym artykule zajmiemy się problemem bezpośrednio, nakreślając definicje kontekstu i zakresu, analizując dwie metody JavaScript, które pozwalają nam manipulować kontekstem, i kończąc głębokim zanurzeniem w skuteczne rozwiązanie dziewięćdziesięciu procent problemów, na które natrafiłem.

Gdzie ja jestem? I kim jesteś?

Każda część Twojego programu JavaScript jest wykonywana w jednym lub innym kontekście wykonania. Możesz myśleć o tych kontekstach jako o sąsiedztwie twojego kodu, dając każdej linii zrozumienie, skąd pochodzi i kim są jego przyjaciele i sąsiedzi. Jak się okazuje, jest to ważna informacja, ponieważ towarzystwa JavaScript mają dość surowe reguły dotyczące tego, kto może się z nimi kojarzyć; konteksty wykonawcze są lepiej traktowane jako społeczności zamknięte, niż otwarte poddziały.

Możemy ogólnie określać te granice społeczne jako zakres, a są one na tyle ważne, aby zostały skodyfikowane w karcie każdego sąsiedztwa, którą określimy jako łańcuch zasięgu kontekstu. Kod w określonym sąsiedztwie może uzyskiwać dostęp tylko do zmiennych wymienionych w jego łańcuchu zasięgu i preferuje interakcję z miejscowymi ze stowarzyszeniami spoza sąsiedztwa.

W praktyce ocena funkcji tworzy odrębny kontekst wykonania, który dołącza jej zasięg lokalny do łańcucha zasięgu, w którym został zdefiniowany. JavaScript rozpoznaje identyfikatory w określonym kontekście, wspinając się na łańcuch zasięgu, przenosząc się lokalnie na globalnie. Oznacza to, że zmienne lokalne o tej samej nazwie co zmienne znajdujące się wyżej w łańcuchu zasięgu mają pierwszeństwo, co ma sens: jeśli moi dobrzy przyjaciele rozmawiają razem o „Mike West”, to całkiem jasne, że mówią o mnie, a nie o piosenkarz bluegrass lub profesor książęcy, mimo że te dwa ostatnie są (prawdopodobnie) lepiej znane.

Przejrzyjmy przykładowy kod, aby zbadać konsekwencje:

Nasza globalna gwiazda ima_celebrity jest rozpoznawalna przez wszystkich. Jest aktywna politycznie, dość często rozmawia z prezydentem i niezwykle przyjazna; będzie podpisywać autografy i odpowiadać na pytania dla każdego, kogo spotka. To powiedziawszy, nie ma zbyt wielu osobistych kontaktów ze swoimi fanami. Jest prawie pewna, że ​​istnieją i że prawdopodobnie mają gdzieś własne życie, ale z pewnością nie wie, co robią, ani nawet ich imion.

W przyjemnym mieście the_mayor to znana twarz. Zawsze chodzi ulicami swojego miasta, rozmawia z wyborcami, podaje ręce i całuje dzieci. Ponieważ Pleasantville to duża, ważna okolica, ma duży czerwony telefon w swoim biurze, co daje jej bezpośredni kontakt z prezydentem (lub przynajmniej najlepszym asystentem) 24 godziny na dobę, 7 dni w tygodniu. Widziała lonely_house na wzgórzu na obrzeżach miasta, ale nigdy tak naprawdę nie martwiła się, kto mieszka w środku.

Ten samotny dom to świat sam w sobie. Agorafobia pozostaje w środku przez większość czasu, grając w pasjansa i karmiąc kota. Dzwonił do the_mayor kilka razy, aby zapytać o lokalne przepisy dotyczące hałasu, a nawet napisał ima_celebrity (to znaczy ima_celebrity Pleasantville) po mailach od fanów po zobaczeniu jej w lokalnych wiadomościach.
to? Co to jest?

Oprócz ustanowienia łańcucha zasięgu, każdy kontekst wykonania oferuje słowo kluczowe o nazwie to. W swoim najczęstszym użyciu służy to jako funkcja tożsamości, zapewniając naszym dzielnicom sposób na odniesienie się do siebie. Jednak nie zawsze możemy polegać na takim zachowaniu: w zależności od tego, jak dotrzemy do konkretnej dzielnicy, może to oznaczać coś zupełnie innego. W rzeczywistości sposób, w jaki docieramy do okolicy, jest dokładnie tym, do czego to ogólnie odnosi się. Na szczególną uwagę zasługują cztery scenariusze:

Wywoływanie metody obiektu W typowym programowaniu obiektowym potrzebujemy sposobu identyfikacji i odniesienia do obiektu, z którym obecnie pracujemy. służy to celowi w sposób godny podziwu, zapewniając naszym przedmiotom możliwość zbadania się i wskazania własnych właściwości. 

<script type = "text / javascript">
var deep_thought = {
odpowiedź: 42,
ask_question: function () {
zwróć to.the_answer;
}
};

var the_meaning = deep_thought.ask_question ();
</script>

W tym przykładzie buduje się obiekt o nazwie deep_thought, ustawia jego właściwość the_answer na 42 i tworzy metodę ask_question. Kiedy wykonywana jest funkcja deep_thought.ask_question (), JavaScript ustanawia kontekst wykonania dla wywołania funkcji, ustawiając go na obiekt, do którego odwołuje się to, co pojawiło się przed ostatnim ”.”, W tym przypadku: deep_thought. Metoda może następnie spojrzeć w lustro przez to, aby zbadać własne właściwości, zwracając wartość zapisaną w tym. The_answer: 42.
ConstructorLikewise, podczas definiowania funkcji, która ma być używana jako konstruktor z nowym słowem kluczowym, można jej użyć w odniesieniu do tworzonego obiektu. Przepiszmy powyższy przykład, aby odzwierciedlić ten scenariusz: <script type = "text / javascript">
funkcja BigComputer (odpowiedź) {
this.the_answer = odpowiedz;
this.ask_question = function () {
zwróć to.the_answer;
}
}

var deep_thought = new BigComputer (42);
var the_meaning = deep_thought.ask_question ();
</script>

Zamiast jawnie tworzyć obiekt deep_thought, napiszemy funkcję do tworzenia obiektów BigComputer i utworzymy instancję deep_thought jako zmienną instancji za pomocą nowego słowa kluczowego. Po uruchomieniu nowego BigComputer (), w tle tworzony jest całkowicie nowy obiekt. Nazywa się BigComputer, a jego słowo kluczowe jest ustawione tak, aby odwoływało się do tego nowego obiektu. Funkcja może ustawić właściwości i metody, które są przezroczyście zwracane na końcu wykonywania BigComputer.

Zauważ jednak, że deep_thought.the_question () nadal działa tak jak wcześniej. Co się tam dzieje? Dlaczego to oznacza coś innego w pytaniu niż w BigComputer? Mówiąc prościej, weszliśmy do BigComputer za pomocą nowego, więc oznaczało to „nowy obiekt”. Z drugiej strony, weszliśmy do pytania poprzez deep_ thinkt, więc kiedy wykonujemy tę metodę, oznacza to „cokolwiek głębokie przemyślenie odnosi się”. nie jest to odczytywane z łańcucha zasięgu, jak inne zmienne, ale jest resetowane na podstawie kontekstu na podstawie kontekstu.
Wywołanie funkcji Co zrobić, jeśli wywołujemy zwykłą, codzienną funkcję bez żadnego z tych wymyślnych obiektów? Co to oznacza w tym scenariuszu? <script type = "text / javascript">
funkcja test_this () {
zwróć to;
}
var i_wonder_what_this_is = test_this ();
</script>

W tym przypadku nie otrzymaliśmy kontekstu od nowego, ani nie otrzymaliśmy kontekstu w postaci obiektu, na który można nałożyć piggyback. Tutaj ten /files/include/default.csss odnosi się do najbardziej globalnej rzeczy, jaką może: dla stron internetowych jest to obiekt okna.
Obsługa zdarzeń Aby bardziej skomplikować zwrot normalnego wywołania funkcji, powiedzmy, że używamy funkcji do obsługi zdarzenia onclick. Co to znaczy, gdy zdarzenie wyzwala wykonanie naszej funkcji? Niestety, nie ma prostej odpowiedzi na to pytanie. Jeśli piszemy moduł obsługi zdarzeń w wierszu, odnosi się to do globalnego obiektu okna:

 <script type = "text / javascript">
funkcja click_handler () {
alert (ten); // ostrzega obiekt okna
}
</script>
...
<button id = 'thebutton' onclick = 'click_handler ()'> Kliknij mnie! </button>

Kiedy dodajemy moduł obsługi zdarzeń za pomocą JavaScript, odnosi się to do elementu DOM, który wygenerował zdarzenie. (Uwaga: Pokazana tutaj obsługa zdarzeń jest krótka i czytelna, ale poza tym słaba. Zamiast tego użyj prawdziwej funkcji addEvent.):

 <script type = "text / javascript">
funkcja click_handler () {
alert (ten); // ostrzega przycisk DOM węzła
}

funkcja addhandler () {
document.getElementById („przycisk”). onclick = moduł obsługi kliknięć;
}

window.onload = addhandler;
</script>
…
<button id = „przycisk”> Kliknij mnie! </button>

Powikłania

Chodźmy na ten ostatni przykład jeszcze przez chwilę. Co jeśli zamiast uruchamiać moduł obsługi kliknięć, chcielibyśmy zadać pytanie głęboko za każdym razem, gdy klikniemy przycisk? Kod tego wydaje się dość prosty; możemy spróbować tego:

Idealnie, prawda? Klikamy przycisk, wykonywane jest deep_thought.ask_question i otrzymujemy „42.”. Dlaczego więc przeglądarka podaje nam niezdefiniowane? Co zrobiliśmy źle?

Problem jest po prostu następujący: przekazaliśmy odniesienie do metody ask_question, która, gdy jest wykonywana jako moduł obsługi zdarzeń, działa w innym kontekście niż wtedy, gdy jest wykonywana jako metoda obiektowa. Krótko mówiąc, to słowo kluczowe w ask_question wskazuje element DOM, który wygenerował zdarzenie, a nie obiekt BigComputer. Element DOM nie ma właściwości the_answer, dlatego wracamy niezdefiniowane zamiast „42.” setTimeout wykazuje podobne zachowanie, opóźniając wykonanie funkcji, jednocześnie przenosząc ją do kontekstu globalnego.

Ten problem pojawia się wszędzie w naszych programach i jest to niezwykle trudny problem do debugowania bez uważnego śledzenia tego, co dzieje się we wszystkich rogach programu, szczególnie jeśli Twój obiekt ma właściwości, które istnieją na elementach DOM lub obiekt okna.
Manipulowanie kontekstem za pomocą .apply () i .call ()

Naprawdę chcemy móc zadać głębokie pytanie, kiedy klikamy przycisk, a bardziej ogólnie, chcemy móc wywoływać metody obiektowe w ich natywnym kontekście, odpowiadając na takie rzeczy, jak zdarzenia i wywołania setTimeout. Dwie mało znane metody JavaScript, zastosowanie i wywołanie, pośrednio włączają tę funkcjonalność, umożliwiając nam ręczne zastąpienie tej wartości /files/include/default.css podczas wykonywania wywołania funkcji. Najpierw spójrzmy na telefon:

W tym przykładzie najpierw definiujemy dwa obiekty, first_object i second_object, każdy z właściwością num. Następnie definiujemy funkcję mnożenia, która akceptuje pojedynczy argument i zwraca iloczyn tego argumentu oraz właściwość num tego obiektu. Gdybyśmy wywołali tę funkcję samodzielnie, zwrócona odpowiedź prawie na pewno byłaby niezdefiniowana, ponieważ globalny obiekt okna nie ma właściwości num, chyba że ją jawnie ustawimy. Potrzebujemy jakiegoś sposobu na powiedzenie, do czego powinno odnosić się to słowo kluczowe; metoda wywołania funkcji mnożenia jest dokładnie tym, czego szukamy.

Pierwszy argument, który należy wywołać, określa, co to oznacza w wykonywanej funkcji. Pozostałe argumenty do wywołania są przekazywane do wykonywanej funkcji, tak jakbyś sam ją nazwał. Kiedy więc wykonywany jest multiply.call (first_object, 5), wywoływana jest funkcja multiply, 5 jest przekazywany jako pierwszy argument, a to słowo kluczowe jest ustawione tak, aby odwoływało się do obiektu first_object. Podobnie, gdy wykonywane jest multiply.call (drugi_obiektyw, 5), wywoływana jest funkcja mnożenia, 5 jest przekazywany jako pierwszy argument, a to słowo kluczowe jest ustawione tak, aby odwoływało się do obiektu drugi_obiektyw.

Apply działa w dokładnie taki sam sposób jak call, ale pozwala zawijać argumenty do wywoływanej funkcji w tablicy, co może być bardzo przydatne podczas programowego generowania wywołań funkcji. Replikacja funkcjonalności, o której właśnie rozmawialiśmy o zastosowaniu Apply, jest banalna:

Zastosuj i zadzwoń są bardzo przydatne same w sobie i warte trzymania w swoim zestawie narzędzi, ale dostają nas tylko w połowie drogi do rozwiązania problemu przesunięć kontekstu dla procedur obsługi zdarzeń. Łatwo jest myśleć, że możemy rozwiązać problem, po prostu używając wywołania, aby zmienić znaczenie tego, gdy konfigurujemy moduł obsługi:

funkcja addhandler () {
var deep_thought = new BigComputer (42),
the_button = document.getElementById (‚thebutton’);

the_button.onclick = deep_thought.ask_question.call (deep_thought);
}

Problem z tym rozumowaniem jest prosty: wywołanie natychmiast wykonuje funkcję. Zamiast podawać odwołanie do funkcji w module obsługi onclick, podajemy wynik wykonanej funkcji. Musimy wykorzystać inną funkcję JavaScript, aby naprawdę rozwiązać ten problem.
The Beauty of .bind ()

Nie jestem wielkim fanem szkieletu JavaScript Prototype, ale jestem pod wielkim wrażeniem jakości jego kodu jako całości. W szczególności jeden prosty dodatek, który wprowadza do obiektu Function, miał ogromny pozytywny wpływ na moją zdolność do zarządzania kontekstem, w którym wykonywane są wywołania funkcji: bind wykonuje to samo ogólne zadanie co wywołanie, zmieniając kontekst, w którym funkcja jest wykonywana. Różnica polega na tym, że bind zwraca odwołanie do funkcji, które może być użyte później, niż wynik natychmiastowego wykonania, które otrzymujemy z wywołaniem.

Jeśli nieco uprościmy funkcję wiązania, aby uzyskać kluczowe pojęcia, możemy wstawić ją do omawianego wcześniej przykładu mnożenia, aby naprawdę zagłębić się w jej działanie; to dość eleganckie rozwiązanie:

Najpierw definiujemy first_object, second_object i funkcję mnożenia, tak jak poprzednio. Po tym, jak się nimi zajmiemy, przechodzimy do tworzenia metody wiązania na prototypie obiektu Function, która powoduje udostępnianie wiązania dla wszystkich funkcji w naszym programie. Po wywołaniu metody multiply.bind (first_object) JavaScript tworzy kontekst wykonania dla metody bind, ustawiając ją na funkcję mnożenia i ustawiając pierwszy argument obj na odwołanie do pierwszego obiektu. Jak na razie dobrze.

Prawdziwym geniuszem tego rozwiązania jest stworzenie metody równej temu (sama funkcja mnożenia). Gdy funkcja anonimowa jest tworzona w następnym wierszu, metoda jest dostępna za pośrednictwem łańcucha zasięgu, podobnie jak obj (nie można jej tutaj użyć, ponieważ po uruchomieniu nowo utworzonej funkcji zostanie ona nadpisana przez nowy, lokalny kontekst ). Ten alias umożliwia użycie polecenia Apply do wykonania funkcji mnożenia, przekazując obj, aby upewnić się, że kontekst jest ustawiony poprawnie. W dziedzinie informatyki temp jest zamknięciem, które po zwróceniu na końcu wywołania wiązania może być użyte w dowolnym kontekście do wykonania mnożenia w kontekście pierwszego_obiektu.

Właśnie tego potrzebujemy w przypadku obsługi zdarzenia i scenariuszy setTimeout omówionych powyżej. Poniższy kod rozwiązuje ten problem całkowicie, wiążąc metodę deep_thought.ask_question z kontekstem deep_thought, aby działał poprawnie po każdym uruchomieniu zdarzenia:

funkcja addhandler () {
var deep_thought = new BigComputer (42),
the_button = document.getElementById (‚thebutton’);

the_button.onclick = deep_thought.ask_question.bind (deep_thought);
}