ECMA-262 JavaScript - the Core.

Wersja angielska: JavaScript the Core.

Autor: Dmitry Soshnikov

Tłumaczenie: Robert Szołdrowski


 

  1. Obiekt
  2. Łańcuch prototypów (a prototype chain)
  3. Konstruktor
  4. Stos kontekstów wykonania (Execution context stack)
  5. Kontekst wykonania – jako obiekt. (Execution context)
  6. Variable object
  7. Activation object
  8. Łańcuch zasięgu. (Scope chain)
  9. Domknięcia - closures
  10. Wartość "this"
  11. Podsumowanie

Ten rozdział jest przeglądem oraz podsumowaniem serii artykułów “ECMA-262-3 in detail” utworzonej przez Dmitriego Soshnikova. Każda z części tego artykułu zawiera odnośniki do właściwego, pod względem tematycznym rozdziału (wersja w języku angielskim). Polecam je przeczytać - jeżeli będziesz potrzebował głębszych wyjaśnień.

Grupa docelowa: doświadczeni programiści, profesjonaliści.

Zaczniemy od rozważenia pojęcia obiektu, który to stanowi fundament dla ECMAScript.

1. Obiekt.

 ECMAScript, jest wysoko abstrakcyjnym, obiektowo zorientowanym językiem programowania, operującym na obiektach. W języku tym istnieją także typy proste (primitives), ale one także w razie potrzeby, mogą być przekonwertowane na obiekty.

Obiekt jest kolekcją właściwości i posiada pojedynczy prototyp obiektu. Prototyp może być obiektem lub wartością null.


Na początku zajmijmy się prostym przykładem obiektu. Prototyp obiektu jest wskazywany przez wewnętrzną właściwość [[Prototype]]. Aczkolwiek, na rysunku używamy notacji z podkreśleniami __<internal-property>__  zamiast notacji z podwójnymi nawiasami, szczególnie dla prototypu obiektu: __proto__.
Dla poniższego kodu:

  1.  var foo = {
  2.    x: 10,
  3.    y: 20
  4.  };

Mamy strukturę z wyłącznie własnymi właściwościami oraz jedna domniemaną (one implicit) właściwością  __proto__ , która jest referencją do prototypu foo:

Rysunek 1. Prosty obiekt z jego prototypem.
Rysunek 1. Prosty obiekt z jego prototypem.

Po co są potrzebne te prototypy? W następnym rozdziale rozważymy koncepcję „łańcucha prototypów” (prototype chain) aby odpowiedzieć na to pytanie.

 

2. Łańcuch prototypów (a prototype chain).

Obiekty prototypu są także obiektami i mogą mieć własne prototypy. Jeżeli prototyp ma referencję do swojego prototypu, a ten także posiada referencję do prototypu, takie połączenie jest nazywane łańcuchem prototypów.

 

Łańcuch prototypów jest skończonym łańcuchem obiektów, który jest używany do dziedziczenia i udostępniania właściwości.

Rozważmy przypadek gdy mamy dwa obiekty, które różnią się między sobą tylko w pewnej małej części. Pozostałe części, dla obu obiektów są takie same. Oczywiście, w dobrze zaprojektowanym systemie, chcielibyśmy ponownie użyć podobne funkcjonalności bez powtarzania ich w każdym z obiektów.

W systemach opartych o klasy (class-based systems), takie współdzielenie kodu jest nazywane dziedziczeniem - wstawiamy podobne funkcjonalności do klasy A i tworzymy klasy B i C, które to dziedziczą z A oraz mają własne małe dodatkowe różnice.

ECMA Script nie jest oparty o koncept klas. Aczkolwiek, styl powtórnego użycia kodu zbytnio się nie różni (chociaż, pod kilkoma względami, jest bardziej elastyczny niż systemy oparte o klasy) i jest osiągany za pomocą łańcucha prototypów. Ten sposób dziedziczenia jest nazywany dziedziczeniem bazującym na delegacjach.

Podobnie jak w przypadku z klasami A, B i C w ECMAScript tworzymy obiekty: a,b i c. Chociaż obiekt a przechowuje tę część, która jest wspólna dla obiektów b i c. Więc obiekty b i c przechowują swoje własne dodatkowe właściwości i metody.

  1. var a = {
  2.   x: 10,
  3.   calculate: function (z) {
  4.     return this.x + this.y + z
  5.   }
  6. };
  7.  
  8. var b = {
  9.   y: 20,
  10.   __proto__: a
  11. };
  12.  
  13. var c = {
  14.   y: 30,
  15.   __proto__: a
  16. };
  17.  
  18. // wywołanie odziedziczonych metod
  19. b.calculate(30); // 60
  20. c.calculate(40); // 80

 

Proste, czyż nie? Widzimy że b i c mają dostęp do metody calculate , która to jest zdefiniowana w obiekcie a. Osiągamy to poprzez mechanizm łańcucha prototypu.

 

To jest prosta zasada: jeżeli pewna właściwość lub metoda nie jest znaleziona w obiekcie (tzn. obiekt nie ma swojej właściwości), to wtedy podjęta zostaje próba znalezienia tej właściwości/metody w łańcuchu prototypów. Jeżeli Właściwość nie jest odnaleziona w prototypie, to wtedy rozważany jest nadrzędny prototyp (prototyp prototypu). Działanie to jest przeprowadzone poprzez cały łańcuch prototypów (to samo jest wykonywane w dziedziczeniu bazującym na klasach, kiedy rozważamy odziedziczoną metodę – wtedy podążamy poprzez, odpowiedni dla tych klas, łańcuch dziedziczenia.) Użyta zostaje pierwsza znaleziona metoda/właściwość o tej samej nazwie. Zatem, odnaleziona właściwość jest nazywana właściwością odziedziczoną. Może się zdarzyć iż właściwość nie zostanie odnaleziona w całym łańcuchu prototypów, w takim przypadku zostanie zwrócona wartość undefined (niezdefiniowana).

 

Zauważmy że wartość this (metoda calculate w powyższym kodzie) użyta w odziedziczonej metodzie jest ustawiona nie na obiekt-prototyp ale na na oryginalny obiekt, w którym ta metoda została odnaleziona. W powyższym przykładzie wartość this.y jest pobierana z obiektów b oraz c ale nie jest pobierana z a. Jednak wartość this.x jest pobrana z obiektu a, za pomocą mechanizmu łańcucha prototypu .

 

Jeżeli dla obiektu nie jest jawnie określony prototyp, to w takim przypadku (obiekt a) wartość __proto__ tego obiektu zostanie ustawiona na Object.prototype.

Sam Object.prototype, także ma właściwość __proto__ i stanowi ostateczne połączenie – jest nią null .

 

Poniższy obraz pokazuje hierarchię dziedziczenia naszych obiektów a, b oraz c:

Łańcuch prototypów.
Rysunek 2. Łańcuch prototypów.
Uwaga: W języku ES5 istnieje alternatywny ustandaryzowany sposób dziedziczenia za pomocą funkcji Object.create.

var b = Object.create(a, {y: {value: 20}});
var c = Object.create(a, {y: {value: 30}});

Więcej informacji o nowym API w języku ES5 jest w tym odpowiednim rozdziale.
Tym niemniej w języku ES6 ustandaryzowano właściwość __proto__ i może być użyta przy inicjalizacji obiektów.

Często istnieje potrzeba aby mieć obiekty z tą samą lub podobną strukturą pól/stanu (state structure), przykładowo, ten sam zestaw właściwości ale z innym wartościami pól. W takim przypadku używamy funkcji konstruktora obiektu według określonego wzoru.

 

3. Konstruktor.

Poza tworzeniem obiektów według określonego wzorca, funkcja konstruktora potrafi wykonać inną pożyteczną rzecz – mianowicie automatycznie ustawia referencję do obiektu prototypu (prototype object ) dla nowo utworzonych obiektów. Referencja do obiektu prototypu jest przechowywana we właściwości ConstructorFunction.prototype.

Przepiszmy poprzedni przykład z obiektami b oraz c do tworzenia których użyjemy funkcji konstruktora. Zatem, znaczenie obiektu a (prototypu) Foo.prototype polega na:

  1. // funkcja konstruktora
  2. function Foo(y) {
  3.   // która tworzy obiekty
  4.   // według określonego wzoru, obiekty te 
  5.   // po utworzeniu mają swoją właściwość "y"
  6.   this.y = y;
  7. }
  8.  
  9. // także "Foo.prototype" przechowuje referencje
  10. // do prototypu nowo utworzonych obiektów,
  11. // także możemy użyć ich do definiowania podziału/dziedziczenia
  12. // właściwości i metod, tak więc podobnie jak 
  13. // w poprzednim przykładzie, teraz mamy: 
  14.  
  15. // odziedziczoną właściwość "x"
  16. Foo.prototype.x = 10;
  17.  
  18. // oraz odziedziczoną metodę "calculate"
  19. Foo.prototype.calculate = function (z) {
  20.   return this.x + this.y + z;
  21. };
  22.  
  23. // teraz tworzymy obiekty "b" oraz "c"
  24. // używając "wzorca" Foo
  25. var b = new Foo(20);
  26. var c = new Foo(30);
  27.  
  28. // wywołanie odziedziczonych metod
  29. b.calculate(30); // 60
  30. c.calculate(40); // 80
  31.  
  32. // sprawdźmy czy referencje
  33. // zwrócą nam oczekiwane wartości
  34.  
  35. console.log(
  36.  
  37.   b.__proto__ === Foo.prototype, // true
  38.   c.__proto__ === Foo.prototype, // true
  39.  
  40.   // także "Foo.prototype" automatycznie tworzy
  41.   // specjalną właściwość "constructor", która to sama jest
  42.   // referencją do funkcji konstruktora;
  43.   // instancje "b" oraz "c" można znaleźć poprzez
  44.   // delegacje oraz sprawdzenie ich własności konstruktora.
  45.  
  46.   b.constructor === Foo, // true
  47.   c.constructor === Foo, // true
  48.   Foo.prototype.constructor === Foo, // true
  49.  
  50.   b.calculate === b.__proto__.calculate, // true
  51.   b.__proto__.calculate === Foo.prototype.calculate // true
  52.  
  53. );

Kod można zobrazować następującym powiązaniem elementów:

Łańcuch prototypów.
Rysunek 3 . Relacja elementów konstruktora i obiektów.

Powyższy obraz, ponownie pokazuje to że każdy obiekt ma prototyp. Konstruktor Foo także ma własne pole __proto__ , które przechowuje referencje na Function.prototype  a funkcja ta z kolei odwołuje się, poprzez swoją właściwość __proto__, do Object.prototype. Aczkolwiek jedna z właściwości Foo wyraźnie wskazuje na  Foo.prototype, podobnie jak też obiekty a i b wskazują na Foo.prototype.

Jeśli przypomnimy sobie pojęcie klasyfikacji obiektów (classification) to zauważymy że właśnie utworzyliśmy klasę (class) nowego oddzielnego obiektu Foo, która to scala prototyp i funkcję konstruktora obiektu.
Przykładowo, dynamiczne typy pierwszo-klasowe (first-class) w języku Python mają dokładnie taką samą implementację podziału dla właściwosci/metod. Klasy w języku Python, z punktu widzenia ECMAScript są ”lukrem syntaktycznym” dla dziedziczenia opartego o delegacje.

 

Uwaga: w ES6 pojęcie klasy (class) zostało ustandaryzowane i jest zaimplementowane jako lukier syntaktyczny (syntactic sugar), tak jak przedstawiono to poniżej na początku funkcji konstruktora. Z tego punktu widzenia łańcuchy prototypów są implementacją szczegółów dziedziczenia bazującego na klasach.

  1. // ES6
  2. class Foo {
  3.   constructor(name) {
  4.     this._name = name;
  5.   }
  6.  
  7.   getName() {
  8.     return this._name;
  9.   }
  10. }
  11.  
  12. class Bar extends Foo {
  13.   getName() {
  14.     return super.getName() + ' Doe';
  15.   }
  16. }
  17.  
  18. var bar = new Bar('John');
  19. console.log(bar.getName()); // John Doe

Kompletne, szczegółowe wyjaśnienie tego tematu można odnaleźć w rozdziale 7 serii o ES3. Są tam dwie części Chapter 7.1. OOP. The general theory, gdzie znajdziesz opis różnorodnych paradygmatów oraz stylistyki na temat OOP, a także porównanie z ECMAScript. Rozdział 7.2 (Chapter 7.2. OOP. ECMAScript implementation) jest szczególnie poświęcony tematowi OOP w ECMAScript.

Teraz, jak już mamy podstawowe wyobrażenie o obiektach, rozważmy jak zaimplementowano runtime program execution (mechanizm uruchomieniowy) w ECMAScript. Pojęcie execution context stack (stos kontekstów wykonania), oznacza że każdy element, w sensie abstrakcyjnym, może być reprezentowany jako obiekt. EcmaScript, prawie wszędzie operuje z użyciem pojęcia obiektu.

4. Stos kontekstów wykonania. (Execution context stack)

Istnieją trzy typy kodu ECMAScript: kod globalny, kod funkcyjny oraz kod eval. Kazdy rodzaj takiego kodu działa w ramach swojego kontekstu wykonania. Istnieje tylko jeden globalny kontekst wykonania i może być wiele kontekstów dla wielu instancji funkcji oraz funkcji typu eval. Każde wywołanie funkcji powoduje uruchomienie kodu w ramach kontekstu tej funkcji oraz wykrycie typu kodu funkcji. Wywołanie funkcji eval, podobnie jak wyżej, powoduje wykrycie typu kodu oraz zagnieżdżenie jej w ramach swojego kontekstu.

 

Zauważ że jedna funkcja może wygenerować nieskończenie długi zestaw kontekstów, ponieważ każe wywołanie funkcji (nawet jeżeli jest to rekurencja) tworzy nowy kontekst z nowym kontekstem stanu:

 

  1. function foo(bar) {}
  2.  
  3. // wywołanie tej samej funkcji,
  4. // w zalezności od podanego argumentu "bar",
  5. // wygeneruje dla tych wywołań
  6. // trzy różne
  7. // konteksty wykonania
  8.  
  9. foo(10);
  10. foo(20);
  11. foo(30);

 

Kontekst wykonania może aktywować kolejny kontekst, na przykład funkcja wywołuje kolejną funkcję (lub tez kontekst globalny wywołuje funkcję globalną.) i tak dalej.

Stos zawierający konteksty wywołania jest nazywany execution context stack, czyli jest to stos kontekstów wykonania.

Konteksty, ze względu na charakter działania, można podzielić na wywołujące (caller) oraz wywoływane (callee). Z kolei kontekst wywoływany może wywoływać kolejny kontekst. Na przykład funkcja wywołana z kontekstu globalnego może wywołać jakieś funkcje wewnętrzne.

Kiedy wywołujący (caller) aktywuje wywoływanego (callee), kontekst wywołujący zawiesza swoje działanie i przekazuje kontrolę (control flow) do kontekstu wywoływanego. Kontekst wywołany jest "wstawiany" na górę stosu i staje się aktywnym kontekstem. Po tym jak aktywny kontekst skończy działanie, sterowanie wywołaniem jest przekazywane do wywołującego, który to kontynuuje, wcześniej przerwane działanie. Wywołujący ponownie może aktywować wywoływanego i powyższy proces może się powtarzać. Wywoływany może po prostu zwrócić sterowanie lub zakończyć działanie wyjątkiem. Rzucony ale nie złapany wyjątek może zakończyć jeden lub wiele kontekstów.

Całą przestrzeń uruchomieniową programów (program runtime) w ECMAScript można przedstawić jako stos kontekstów wykonania [execution context (EC) stack] , gdzie aktywny kontekst jest umieszczony na górze stosu:

 

Rysunek 4. Stos kontekstów wykonania.
Rysunek 4. Stos kontekstów wykonania.

Kiedy program zaczyna działanie, osadza się na w globalnym kontekście wykonania, który jest pierwszym elementem na spodzie stosu. Wtedy kod globalny wykonuje potrzebne inicjalizacje, tworzy obiekty i funkcje. Podczas wykonywania globalnego kontekstu (global execution context ), jego kod może aktywować inne (już stworzone) funkcje, które wejdą w jego kontekst wykonania (execution context ), ustawiając nowy kontekst wywołania na stosie, itd. Kiedy inicjalizacja zostanie wykonana, system uruchomieniowy (runtime system) czeka na jakieś zdarzenie (np. kliknięcie myszką przez uzytkownika), które to aktywuje jakieś funkcje i położy nowy kontekst wywołania na stosie.

Na poniższym rysunku, mamy jakąś funkcje w kontekście wykonania oznaczonym jako EC1, oraz kontekst globalny oznaczony jako Global EC. Rysunek ten przedstawia modyfikację stosu po aktywowaniu i deaktywowaniu EC1 przez globalny kontekst:

 

Rysunek 5. Zmiany na stosie kontekstów wykonania.
Rysunek 5. Zmiany na stosie kontekstów wykonania.

Powyżej zostało zobrazowane w jaki sposób ECMAScript zarządza wykonaniem kodu. Więcej informacji o kontekście wykonania można ECMAScript można znaleźć w odpowiednim dziale Chapter 1. Execution context.

 

Tak jak wcześniej wspomniano, każdy kontekst wywołania na stosie może być opisany jako obiekt. Spójrzmy na jego strukturę i rodzaj właściwości jakie to kontekst potrzebuje do wykonania swego kodu.

 

5. Kontekst wykonania – jako obiekt. (Execution context)

 

Kontekst wykonania, można sobie wyobrazić jako prosty obiekt. Każdy kontekst wywołania ma zestaw właściwości (nazywanych stanami kontekstu) niezbędny do śledzenia procesu wykonania własnego załączonego kodu. Poniżej jest rysunek struktury kontekstu wykonania:

Rysunek 6. Kontekst wykonania.
Rysunek 6. Kontekst wykonania.

Po za tymi trzema niezbędnymi właściwościami (variable object, this oraz scope chain - łańcuch zasięgu), kontekst wywołania może mieć dodatkowe stany – jest to uzależnione od implementacji.

Rozważmy szczegółowo te ważne właściwości kontekstu.

 

6. Variable object

Variable object - to kontener danych powiązanych z kontekstem wykonania. Jest specjalnym obiektem, który przechowuje zmienne i deklaracje funkcji, zdefiniowane w tym kontekście.

Zwróć uwagę że definicje funkcji (function expressions), w przeciwieństwie do deklaracji funkcji, nie są zawarte w Variable obiect .

Variable object jest pojęciem abstrakcyjnym. W różnych typach kontekstu, jest reprezentowany przy użyciu różnych obiektów. Na przykład, w kontekście globalnym Variable object jest własnym globalnym obiektem, dlatego poprzez właściwości obiektu globalnego mamy dostęp do zmiennych globalnych.

Rozważmy następujący przykład w globalnym kontekście wykonania:

 

  1. var foo = 10;
  2.  
  3. function bar() {} // Deklaracja funkcji, (FD - function declaration)
  4. (function baz() {}); // definicja funkcji, (FE - function expression)
  5.  
  6. console.log(
  7.   this.foo == foo, // true
  8.   window.bar == bar // true
  9. );
  10.  
  11. console.log(baz); // ReferenceError, "baz" is not defined

Dla powyższego przykładu globalny kontekst Variable object (VO) ma następujące właściwości:

Rysunek 7. Globalny VO.
Rysunek 7. Globalny VO (The global variable object).

Spójrzmy ponownie na powyższy kod. Funkcja baz jest definicją funkcji i nie jest zawarta w Variable Object. Gdy z zewnątrz staramy się uruchomić funkcję baz, to wtedy otrzymujemy ReferenceError (błąd referencji).

 

Zauważmy że w przeciwieństwie do innych języków (np. c/c++) w ECMAScript tylko funkcje tworzą nowy zasięg. Zmienne i metody (funkcje wewnętrzne) zawarte w zasięgu funkcyjnym nie są bezpośrednio widoczne na zewnątrz, więc nie zaśmiecają globalnego VO (global Varible Obiect).

 

Używając funkcji eval także wchodzimy w nowy kontekst wykonania (należący do eval). Aczkolwiek eval używa również globalnego Variable Object lub też Variable Object, który należy do wywołującego - caller (np. funkcja z której to eval został wywołany ).

 

Co w takim razie z funkcjami oraz ich Variable Object? Z punktu widzenia funkcji, VO jest dla niej obiektem aktywującym (activation object).

 

7. Activation object - obiekt aktywujący.

Kiedy funkcja jest uruchamiana poprzez przez inny obiekt (caller), wtedy jest tworzony nowy activation object . Obiekt ten jest podobny do variable object ale oprócz pól dla wymaganych parametrów (formal parameters ) posiada specjalne pole na obiekty typu argumenty (arguments), które są zestawem danych indeks-właściwość.

 

Rozpatrzmy poniższy przykład:

 

  1. function foo(x, y) {
  2.   var z = 30;
  3.   function bar() {} // Deklaracja funkcji (FD - function declaration)
  4.   (function baz() {}); // definicja funkcji (FE - function expression)
  5. }
  6.  
  7. foo(10, 20);

W kontekście funkcji foo, mamy następujący activation object (AO):

Rysunek 8. Activation Object.
Rysunek 8. Activation Object.

Zauważmy że deklaracja funkcji baz nie jest zawarta w variable/active object.

Kompletny opis wraz z wszystkimi szczególnymi przypadkami (takiimi jak „hoisting” zmiennych i deklaracji funkcji), można odnaleźć w podobnie brzmiącym rozdziale Chapter 2. Variable object.

 

Zwróć uwagę, iż w ES5 koncepcja variable object oraz activation object są połączone w model środowiska leksykalnego (lexical environments), którego to dokładny opis można odnaleźć w tym odpowiednim rozdziale.

Przejdźmy do następnego rozdziału. Jak wiemy w ECMAScript możemy używać funkcji wewnętrznych, z których to mamy dostęp do zmiennych należących do funkcji zewnętrznych (zmiennych w globalnym kontekście).

Jeżeli przyjmiemy że Variable Object może być obiekt zasięgu w ramach pewnego kontekstu (scope object of the context), to analogicznie do omawianego wyżej łańcucha prototypów, możemy utworzyć z tych obiektów łańcuch zasięgu (scope chain).

 

8. Łańcuch zasięgu. (Scope chain)

 

Łańcuch zasięgu jest to lista obiektów, będąca wynikiem wyszukiwania identyfikatorów pojawiających się w kontekście kodu.

Zasada jest podobna do łańcucha prototypów . Czyli, jeżeli zmienna nie została odnaleziona we własnym zasięgu (we własnym AO/VO – activation/variable object), następuje proces odszukania jej w nadrzędnym VO i ten proces przeszukiwania może przebiegać dalej aż do ostatecznego odnalezienia zmiennej lub zakończy się stwierdzeniem że taka zmienna nie istnieje.

 

Podobnie jak w przypadku kontekstów, identyfikatorami są: nazwy zmiennych, deklaracje funkcji, wymagane parametry (formal parameters) itp. Kiedy funkcja, w swoim kodzie odwołuje się do identyfikatora, który nie jest zmienną lokalną (a także funkcją lub wymaganym parametrem), to taka zmienna jest nazywana zmienną swobodną. Do wyszukiwania zmiennych swobodnych wykorzystuję się łańcuch zasięgu (Scope chain).

 

Generalnie ujmując, łańcuch zasięgu (Scope chain) jest listą składającą się ze wszystkich nadrzędnych VO (parent variable objects) oraz dodatkowo (patrząc od początku łańcucha zasięgu) ze wszystkich VO/AO (variable/activation object) należących do danej funkcji. Aczkolwiek łańcuch zasięgu (Scope chain) może zawierać także inne obiekty, np. obiekty dynamicznie dodane do łańcucha zasięgu podczas wykonywania kontekstu – takie jak obiekty with lub specjalne obiekty klauzuli catch.

 

Podczas wyznaczania identyfikatora, przeszukiwany jest łańcuch zasięgu zaczynający się od AO (activation obiect) i jeśli identyfikator nie zostanie odnaleziony przez funkcję we własnym AV to proces szukania jest przenoszony do elementu nadrzędnego, podobnie jak ma to miejsce w łańcuchu prototypów (prototype chain).

 

  1. var x = 10;
  2.  
  3. (function foo() {
  4.   var y = 20;
  5.   (function bar() {
  6.     var z = 30;
  7.     // "x" oraz "y" są "wolnymi zmiennymi"
  8.     // i są odnalezione (po aktywacji obiektu bar)
  9.     // w następnym obiekcie łańcucha zasięgu,
  10.     // który to (łańcuch) należy do obiektu bar.
  11.     console.log(x + y + z); // --> 60
  12.   })();
  13. })();

Możemy założyć że istnieją połączania w ramach obiektów wchodzących w skład łańcucha zasięgu (Scope chain), poprzez właściwość __parent__, która to wskazuje na następny element w ciągu obiektów/liście obiektów (object in the chain). Taka koncepcja istnieje i może być przetestowana w real Rhino code, dokładnie taka technika jest zastosowana w ES5 lexical environments (nazwana jest połączeniem zewnętrznym outer link). Inna reprezentacja pojęcia scope chain to prosta tablica. Używając pojęcia __parent__, możemy przedstawić powyższy przykład na rysunku znajdującym się poniżej (zatem nadrzędny VA jest zapisany we właściwości [[Scope]]):

Łańcuch zasięgu (Scope chain).
Rysunek 9 Łańcuch zasięgu (Scope chain).

 

Podczas wykonywania kodu, łańcuch zasięgu (Scope chain) może być rozszerzony za pomocą wyrażeń with oraz klauzuli catch. Skoro te obiekty, są obiektami prostymi, więc mogą mieć prototypy oraz łańcuch prototypów. To dowodzi faktu iż wyszukiwanie w łańcuchu zasięgu jest dwuwymiarowe.

  1. Po pierwsze, jest rozważane połączenie istniejące w łańcuchu zasięgu,
  2. następnie w każdym z połączeń łańcucha zasięgu następuje poszukiwanie w głąb łańcucha prototypów  (o ile istnieje połączenie do prototypu).

Dla naszego przykładu:

 

  1. Object.prototype.x = 10;
  2.  
  3. var w = 20;
  4. var y = 30;
  5.  
  6. // w SpiderMonkey obiekt globalny.
  7. // Innymi słowy, VO (Variable object)
  8. // dziedziczy z globalnego kontekstu "Object.prototype",
  9. // tak więc możemy odwoływać się do zmiennej "x", która
  10. // "jest niezdefiniowaną zmienną globalną" - brak "var",
  11. // odnalezioną w łańcuchu prototypu.
  12.  
  13. console.log(x); // --> 10
  14.  
  15. (function foo() {
  16.  
  17.   // "foo" lokalne zmienne
  18.   var w = 40;
  19.   var x = 100;
  20.  
  21.   // "x" jest odnalezione 
  22.   // w "Objekt.prototype"
  23.   // ponieważ {z: 50} z niego dziedziczy
  24.  
  25.   with ({z: 50}) {
  26.     console.log(w, x, y , z); // --> 40, 10, 30, 50
  27.   }
  28.  
  29.   // po tym jak obiekt "with" został usunięty 
  30.   // z łańcucha zasięgu, "x" jest ponownie odnalezione
  31.   //  w AO (Activation Object) kontekstu "foo";
  32.   //  "w" jest także zmienną lokalną
  33.   console.log(x, w); // --> 100, 40
  34.  
  35.   // oto jak możemy odwoływać się do
  36.   // przysłoniętej zmiennej "w"
  37.   // w środowisku przeglądarki
  38.   console.log(window.w); // --> 20
  39.  
  40. })();

Mamy więc następującą strukturę (to znaczy, zanim podążymy za połączeniem we właściwości __parent__, najpierw jest rozważane połączanie łańcucha wyznaczone przez __proto__ ).

 

Rysunek 8. Powiększenie łańcucha zasięgu (Scope chain) o “ obiekt with.
Rysunek 10. Powiększenie łańcucha zasięgu (Scope chain) o obiekt "with".

Zauważ, że nie we wszystkich implementacjach obiekt globalny dziedziczy z Object.prototype. Zachowanie opisane na powyższym rysunku (używając "niezdefiniowanej" zmiennej x z kontekstu globalnego) może być przetestowane na przykład w SpiderMonkey (silnik JavaScipt używany w Firefox).

 

Do momentu, gdy istnieją wszystkie zmienne obiektów nadrzędnych, nie ma nic niezwykłego w pobieraniu danych nadrzędnych przez funkcje wewnętrzne – po prostu podążamy po scope chain odnajdując potrzebne zmienne. Aczkolwiek, tak jak wspomnieliśmy powyżej, po tym jak kontekst się skończy, wszystkie jego stany oraz on sam jest niszczony. W tym samym czasie, może nastąpić powrót z funkcji wewnętrznej do funkcji zewnętrznej. Co więcej, funkcja z której wrócono, może być później aktywowana z innego kontekstu. Co się stanie z taką aktywacją, jeżeli kontekst jakiejś wolnej zmiennej już "nie istnieje"?

Ogólnie dla rozwiązania takiego problemu powstała koncepcja domknięć (closure), która to w ECMAScript jest bezpośrednio powiązana z koncepcją łańcucha zasięgu (scope chain).

 

9. Domknięcia - closures.

 

W ECMAScript, funkcje są obiektami typu pierwszo-klasowego (first-class). To pojęcie oznacza że funkcje mogą być przekazane jako argument do innych funkcji, podobnie jak string czy liczba. W takim przypadku są nazywane „funars” - skrót od „argumentów funkcyjnych” (functional arguments). Funkcje, które otrzymują „funargs” (argumenty funkcyjne) są nazywane funkcjami wyższego rzędu (higher-order functions) lub używając nazw matematycznych – operatorami. Także funkcje mogą być zwrócone z innych funkcji. Funkcje zwracające inne funkcje są nazywane funkcjami z wartością funkcyjną (function valued lub functional value).

 

Istnieją dwa konceptualne (pojęciowe) pod-problemy i są związane z „funargs” oraz „functional values”(„wartościami funkcyjnymi”). Powyższe dwa pod-problemy są łączone w jeden nadrzędny problem zwany „Funarg problem” (problem argumentu funkcyjnego - “A problem of a functional argument”). Właśnie do kompletnego rozwiązania problemu „funarg” zostało utworzone pojęcie domknięć. Gdy zajmiemy się bardziej szczegółowo wyżej wymienionymi dwoma problemami to możemy zauważyć że mają rozwiązanie w ECMAScript, używając pokazanych na rysunku właściwości funkcji [[Scope]]).

 

Pierwszy pod-problemem związanym z „funarg” jest to “upward funarg problem” (gdy funarg jest zwracany do nadrzędnego obiektu). Pojawia się wtedy gdy funkcja jest zwracana do "góry" (na zewnątrz) z innej funkcji i używa, wcześniej już wspomnianych wolnych zmiennych.

 

Aby umożliwić dostęp do zmiennych znajdujących się w nadrzędnym kontekscie, nawet jeśli ten nadrzędny kontekst już się skończył, funkcja wewnętrzna w momencie jej tworzenia, zapisuje odpowiednią wartość we własnym [[Scope]], która to (wartość) jest łańcuchem zasięgu (scope chain) funkcji nadrzędnej.

Gdy funkcja jest ponownie aktywowana, jej łańcuch zasięgu (wynikający z kontekstu tej funkcji) , jest uformowany jako kombinacja AO (Activation Object) oraz zapisane właściwości [[Scope]] (dokładnie to co widzimy poniżej):

 

Scope chain = Activation object + [[Scope]]

 

Zauważ ważną rzecz – dokładnie w momencie tworzenia – funkcja zapisuje nadrzędny łańcuch zasięgu (parent's scope chain), ponieważ ten zachowany łańcuch zasięgu będzie użyty do poszukiwania zmiennych przy okazji  kolejnych wywołań tej funkcji.

 

  1. function foo() {
  2.   var x = 10;
  3.   return function bar() {
  4.     console.log(x);
  5.   };
  6. }
  7.  
  8. // "foo" zwraca także funkcję
  9. // i ta zwrócona funkcja 
  10. // używa wolnej zmiennej "x"
  11.  
  12. var returnedFunction = foo();
  13.  
  14. // zmienna globalna "x"
  15. var x = 20;
  16.  
  17. // wykonanie zwróconej funkcji
  18. returnedFunction(); // --> 10, ale nie 20

 

Ten typ zakresu jest nazywany zakresem leksykalnym lub statycznym (lexical/static scope). Widzimy że zmienna x jest odnaleziona w zapisanym zakresie [[Scope]] zwracanej funkcji bar. W ogólnym rozumieniu istnieje także zakres dynamiczny, wtedy zmienna x z powyższego przykładu otrzymałaby wartość 20 ale nie 10. Aczkolwiek, zakres dynamiczny nie jest używany w ECMAScript.

 

Druga częścią problemu „funarg” jest “downward funarg problem” - czyli problem wynikający z przekazania (funarg) do funkcji wewnętrznej, z funkcji znajdującej się w innym zakresie leksykalnym. W takim przypadku może istnieć kontekst nadrzędny, który może być niejednoznacznie zidentyfikowany. Problemem jest: z którego zasięgu ma być użyty identyfikator zmiennej? Czy statycznie zachowanego podczas tworzenia funkcji czy też dynamicznie utworzonego podczas wykonywania (zasięg funkcji wywołującej - caller)? Aby uniknąć takich dwuznaczności, utworzono domknięcia i użyto statycznego zasięgu:

 

  1. // globalna "x"
  2. var x = 10;
  3.  
  4. // globalna funkcja
  5. function foo() {
  6.   console.log(x);
  7. }
  8.  
  9. (function (funArg) {
  10.  
  11.   // lokalna "x"
  12.   var x = 20;
  13.  
  14.   // nie ma dwuznaczności ponieważ
  15.   // użyliśmy globalnej zmiennej "x",
  16.   // która to została zapisana statycznie
  17.   // w [[Scope]] funkji foo,
  18.   // i nie użyliśmy "x" z zasięgu funkcji wołającej (caller)
  19.   // która aktywuje "funarg"
  20.  
  21.   funArg(); // 10, ale nie 20
  22.  
  23. })(foo); // przekazanie "w dół" funkcji foo jako "funarg"

 

Dochodzimy do wniosku, iż zasięg statyczny jest wymagany aby w języku programowania można było zastosować domknięcia. Aczkolwiek, niektóre języki mogą wspierać kombinację zasięgu dynamicznego oraz zasięgu statycznego, pozostawiając wybór programiście – co domknięcia mają robić a czego nie mogą robić. Odkąd w ECMAScript jest tylko używany statyczny zasięg pojawia się wniosek: ECMAScript całkowicie wspiera domknięcia, które to technicznie są zaimplementowane przy użyciu właściwości [[Scope]], która to właściwość należy do każdej funkcji. Teraz możemy dokładnie zdefiniować domknięcia:

 

Domknięcie jest kombinacją zasięgu bloku kodu (w ECMAScript – zasięg funkcyjny) oraz zapisanego statycznego/leksykalnego zasięgu funkcji nadrzędnych (parent functions). Aczkolwiek, poprzez te zapisane zasięgi, funkcja ma łatwy dostęp do wolnych zmiennych (free variables).

 

Zauważ że odkąd każda (normalna) funkcja zachowuje [[Scope]] w momencie tworzenia, teoretycznie, wszystkie funkcje w ECMAScript są domknięciami.

 

Inną ważną rzeczą do zapamiętania jest fakt iż kilka funkcji może mieć ten sam nadrzędny zasięg (parent scope). Jest to całkowicie normalna sytuacja, gdy przykładowo, mamy dwie wewnętrzne/globalne funkcje. W takim przypadku, zmienne są przechowywane we właściwości [[Scope]] i są dzielone między wszystkimi funkcjami, które to mają ten sam zasięg nadrzędny. Zmiany dokonane na zmiennej przez jedno z domknięć są dzielone między wszystkie funkcje, które mają ten sam nadrzędny zasięg.

 

  1. function baz() {
  2.   var x = 1;
  3.   return {
  4.     foo: function foo() { return ++x; },
  5.     bar: function bar() { return --x; }
  6.   };
  7. }
  8.  
  9. var closures = baz();
  10.  
  11. console.log(
  12.   closures.foo(), // 2
  13.   closures.bar()  // 1
  14. );

Powyższy kod można opisać poniższy rysunkiem:

Rysunek 11. [[Scope]] dzielony między obiekty.
Rysunek 11. [[Scope]] dzielony między obiekty.

Podczas tworzenia kilku funkcji za pomocą pętli, pojawia się pewna dezorientująca cecha. Używając licznika pętli wewnątrz utworzonej funkcji, niektórzy programiści, często otrzymują nieoczekiwane wyniki gdy na przykład wszystkie funkcje mają tą samą wartość licznika wewnątrz funkcji. Jest tak ponieważ wszystkie te funkcje mają ten sam [[Scope]] gdzie licznik pętli ma ostatnio zapisaną wartość.

 

  1. var data = [];
  2.  
  3. for (var k = 0; k < 3; k++) {
  4.   data[k] = function () {
  5.     alert(k);
  6.   };
  7. }
  8.  
  9. data[0](); // 3, ale nie 0
  10. data[1](); // 3, ale nie 0
  11. data[2](); // 3, ale nie 0

Istnieje kilka technik, które mogą pomóc w rozwiązaniu tego problemu. Jedną z technik jest dołączenie dodatkowego obiektu do łańcucha zasięgu (scope chain) – przykład użycia dodatkowej funkcji:

 

  1. var data = [];
  2.  
  3. for (var k = 0; k < 3; k++) {
  4.   data[k] = (function (x) {
  5.     return function () {
  6.       alert(x);
  7.     };
  8.   })(k); // przekazanie wartości "k"
  9. }
  10.  
  11. // teraz jest poprawnie
  12. data[0](); // --> 0
  13. data[1](); // --> 1
  14. data[2](); // --> 2

 

Dla tych, którzy są zainteresowani głębszym poznaniem teorii domknięć oraz jej praktycznego zastosowania, przygotowano dodatkowe materiały, które to można znaleźć pod tym linkiem: Chapter 6. Closures. Do zdobycia większej wiedzy na temat łańcucha zasięgu (scope chain), wato zerknąć tutaj : Chapter 4. Scope chain.

Teraz przechodzimy do następnego rozdziału, rozważymy ostatnią własność kontekstu wykonania (execution context). To jest pojęcie wartości „this”.

 

10. Wartość "this".

Wartość "this" jest specjalnym obiektem, który jest związany z kontekstem wykonania (execution context). Dlatego może być nazywany jako context object, to znaczy obiekt w którego to kontekście jest aktywny kontekst wykonania.

 

Każdy obiekt może użyć „this” jako wartość kontekstu. Chciałbym od razu wyjaśnić nieporozumienia, które powstają przy tworzeniu kodu, związane z kontekstem wykonania zaimplementowanym w ECMAScript a w szczególności z właściwością „this”. Często bowiem wartość „this”, niewłaściwie jest opisywana jako właściwość Variable Object. Zapamiętajmy więc:

 

Wartość „this” jest właściwością kontekstu wykonania ale nie właściwością Variable Object.

 

Ta cecha jest bardzo ważna, ponieważ w przeciwieństwie do zmiennych, wartość „thisnigdy nie uczestniczy w procesie wyszukiwania identyfikatora. Więc, podczas używania „this” w naszym kodzie, jego wartość jest pobierana bezpośrednio z kontekstu wykonania bez sprawdzania łańcucha zasięgu (scope chain). Wartość „this” jest ustalana tylko raz podczas wejścia w kontekst wykonania.

Przykładowo, przeciwnie do ECMAScript, Python ma swój argument self, który jest prostą zmienną podobnie zarządzaną, ale może zostać zmieniona podczas wykonania na inną wartość. Powtórzmy jeszcze raz ważną rzecz. W ECMAScript nie ma możliwości ustawienia nowej wartości dla „this” ponieważ to nie jest zmianna/właściwość i nie istnieje jako właściwość/pole Variable Object.

W kontekście globalnym, wartość „this” jest sama w sobie obiektem globalnym, oznacza to iż w poniższym przykładzie wartość „this” jest równa „Variable Object”:

 

  1. var x = 10;
  2.  
  3. console.log(
  4.   x, // --> 10
  5.   this.x, // --> 10
  6.   window.x // --> 10
  7. );

 

W przypadku kontekstu funkcji, wartość „thisw każdym wywołaniu funkcji może być inna. Tutaj „this” jest otrzymywana z obiektu wywołującego (caller) poprzez formę „call expression” czyli sposób w jaki to funkcja jest aktywowana. Przykładowo poniższa funkcja foo będąca wołaną (callee), jest wołana z globalnego kontekstu i ten kontekst jest wywołującym (caller). Spójrzmy na poniższy przykład. Zauważmy jak ten sam kod funkcji otrzymując różną wartość od wywołującego (caller), zmienia „this” w zależności od różnych wywołań (różnych sposobów aktywacji):

 

  1. // Kod funkcji"foo"
  2. // nigdy się nie zmienia, ale wartość "this"
  3. // zmienia się za każdym uruchomieniem
  4.  
  5. function foo() {
  6.   alert(this);
  7. }
  8.  
  9. // Wołający (caller) aktywuje "foo" - wołanego (callee)
  10. // i ustawia "this" dla wołanego (callee)
  11.  
  12. foo(); // "this" wskazuje na kontekst Objektu globalnego
  13. foo.prototype.constructor(); // "this" wskazuje na kontekst foo.prototype
  14.  
  15. var bar = {
  16.   baz: foo
  17. };
  18.  
  19. bar.baz(); // "this" wskazuje na kontekst "bar"
  20.  
  21. (bar.baz)(); // także "this" wskazuje na kontekst "bar"
  22. (bar.baz = bar.baz)(); // ale tu "this" wskazuje na kontekst Objektu globalnego
  23. (bar.baz, bar.baz)(); // także "this" wskazuje na kontekst Objektu globalnego
  24. (false || bar.baz)(); // nadal "this" wskazuje na kontekst Objektu globalnego
  25.  
  26. var otherFoo = bar.baz;
  27. otherFoo(); // ponownie "this" wskazuje na kontekst Objektu globalnego

 

Dla głębszego zrozumienia jak wartość „this” może się zmienić dla różnych wywołań tej samej funkcji funkcji, proponuję przeczytać Chapter 3. This. Są tam bardziej szczegółowo opisane, wyżej omówione przypadki.

 

11. Podsumowanie

Na tym zakończymy krótki przegląd. Myślę że nie tak bardzo „krótki” :) Aczkolwiek, całe wyjaśnienie wszystkich zagadnień, może złożyć się na kompletną książkę. Nie zostały poruszone dwa główne zagadnienia: funkcje oraz różnice między różnymi typami funkcji. Na przykład deklaracje funkcji i wyrażenia funkcji używane w ECMAScript. Obydwa zagadnienia można odnaleźć w odpowiednich rozdziałach serii o ES3: Chapter 5. Functions oraz Chapter 8. Evaluation strategy. Jeżeli masz komentarze, zapytania lub chcesz coś dodać, zapraszam na blog autora tekstu.

Jako autor tłumaczenia, będę wdzięczny za wskazanie ewentualnych błędów w polskiej wersji.

Powodzenia w nauce ECMAScript!

 

Orginalny tekst: Dmitry A. Soshnikov Opublikowany: 2 września 2010 r.

Tłumaczenie: Robert Szołdrowski. Opublikowany: 10 marca 2016 r.

Od tłumacza. Jeżeli znajdziesz jakiś błąd lub też pewne zdanie jest niezrozumiałe to proszę kierować wiadomości na mój profil w serwisie facebook:  Robert Szołdrowski