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.
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żne 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:
Mamy strukturę z jednoznacznie własnymi właściwościami oraz jedną ukrytą właściwość __proto__, która jest referencją do prototypu foo: foo:
Po co są potrzebne prototypy? W następnym rozdziale rozważymy koncepcje łańcucha prototypów aby odpowiedzieć na to pytanie.
Obiekty prototypu są także obiektami i mogą mieć własne prototypy. Jeżeli prototyp ma referencje do swojego prototypu, a ten także posiada referencje 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ę miedzy sobą tylko w pewnej malej 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 maja 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 roż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ą cześć, która jest wspólna dla obiektów b i c. Wiec obiekty b i c przechowują swoje własne dodatkowe właściwości i metody.
var a = {
x: 10,
calculate: function (z) {
return this.x + this.y + z
}
};
var b = {
y: 20,
__proto__: a
};
var c = {
y: 30,
__proto__: a
};
// wywołanie odziedziczonych metod
b.calculate(30); // 60
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 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, które prowadzi do null .
Poniższy obraz pokazuje hierarchie dziedziczenia naszych obiektów a, b oraz c:
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 jezyku ES6 ustandaryzowano własciwosc __proto__ i możne być użyta przy inicjalizacji obiektów.
Często istnieje potrzeba aby mieć obiekty z tą sama lub podobną struktura 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.
Poza tworzeniem obiektów według określonego wzorca, funkcja konstruktora potrafi wykonać inną pożyteczna rzecz mianowicie automatycznie ustawia referencje 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:
// funkcja konstruktora
function Foo(y) {
// która tworzy obiekty
// według określonego wzoru, obiekty te
// po utworzeniu maja swoja właściwość "y"
this.y = y;
}
// także "Foo.prototype" przechowuje referencje
// do prototypu nowo utworzonych obiektów,
// także możemy ożyć ich do definiowania podzialu/dziedziczenia
// właściwości i metod, tak wiec podobnie jak
// w poprzednim przykładzie, teraz mamy:
// odziedziczoną właściwość "x"
Foo.prototype.x = 10;
// oraz odziedziczoną metodę "calculate"
Foo.prototype.calculate = function (z) {
return this.x + this.y + z;
};
// teraz tworzymy obiekty "b" oraz "c"
// używając "wzorca" Foo
var b = new Foo(20);
var c = new Foo(30);
// wywołanie odziedziczonych metod
b.calculate(30); // 60
c.calculate(40); // 80
// sprawdźmy czy referencje
// zwraca nam oczekiwane wartości
console.log(
b.__proto__ === Foo.prototype, // true
c.__proto__ === Foo.prototype, // true
// także "Foo.prototype" automatycznie tworzy
// specjalną właściwość "constructor", która to sama jest
// referencja do funkcji konstruktora;
// instancje "b" oraz "c" można znaleźć poprzez
// delegacje oraz sprawdzenie ich własności konstruktora.
b.constructor === Foo, // true
c.constructor === Foo, // true
Foo.prototype.constructor === Foo, // true
b.calculate === b.__proto__.calculate, // true
b.__proto__.calculate === Foo.prototype.calculate // true
);
Kod można zobrazować następującym powiązaniem elementów:
Powyższy obraz, ponownie pokazuje to że każdy obiekt ma prototyp. Konstruktor Foo także ma właściwość __proto__ , która przechowuje referencje na Function.prototype a funkcja ta z kolei odwołuje się, poprzez swoja właściwość __proto__, do Object.prototype. Następna właściwość wcześniej wspomnianego konstruktora Foo(napis “prototype” na niebieskim tle) wyraźnie wskazuje na Foo.prototype, podobnie jak też obiekty a i b wskazują na Foo.prototype.
Jeżeli rozważymy koncept klas obiektów (teraz, gdy mamy sklasyfikowaną nową rzecz - Foo) to kombinacja funkcji konstruktora oraz prototypu może być uznana jako klasa obiektu. W języku Python, pierwszoklasowe (first-class) dynamiczne klasy mają dokładnie tę samą implementację ustanawiania właściwości/metod. Z tego puntu widzenia, klasy w python są tylko lukrem syntaktycznym dla dziedziczenia opartego o delegacje, które to jest używane w ECMAScript.
// ES6
class Foo {
constructor(name) {
this._name = name;
}
getName() {
return this._name;
}
}
class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}
}
var bar = new Bar('John');
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 pojęciem obiektu.
Istnieją trzy typy kodu ECMAScript: kod globalny, kod funkcyjny oraz kod eval. Każdy 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żde wywołanie funkcji (nawet jeżeli jest to rekurencja) tworzy nowy kontekst z nowym stanem kontekstu:
function foo(bar) {}
// wywołanie tej samej funkcji,
// w zależności od podanego argumentu "bar",
// wygeneruje dla tych wywołań
// trzy różne
// konteksty wykonania
foo(10);
foo(20);
foo(30);
Kontekst wykonania może aktywować kolejny kontekst, na przykład funkcja wywołuje kolejną funkcje (lub też kontekst globalny wywołuje funkcje 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ła przestrzeń uruchomieniowa 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:
Kiedy program zaczyna działanie to jest uruchamiany w globalnym kontekście wykonania, który to 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 ustawiając nowy kontekst wykonania na stosie, itd. Kiedy inicjalizacja zostanie wykonana, system uruchomieniowy (runtime system) czeka na jakieś zdarzenie (np. klikniecie myszką przez użytkownika), które to aktywuje jakąś funkcje i położy nowy kontekst wywołania na stosie.
Na poniższym rysunku, mamy pewną funkcje w kontekście wykonania oznaczonym jako EC1, oraz kontekst globalny oznaczony jako Global EC. Rysunek ten przedstawia modyfikacje stosu po aktywowaniu i deaktywowaniu EC1 przez globalny kontekst:
Powyżej zostało zobrazowane w jaki sposób ECMAScript zarządza wykonaniem kodu. Więcej informacji o kontekście wykonania ECMAScript można znaleźć w odpowiednim dziale Chapter 1. Execution context.
Tak jak wcześniej wspomniano, każdy kontekst wywołania układany 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 swojego kodu.
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:
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 najważniejsze właściwości kontekstu.
Uwaga od tłumacza: Poniżej, pozostaje nieprzetłumaczone pojęcie "function expression", które to w języku polskim ma kilka nieprecyzyjnych a nawet mylących odpowiedników.
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óc uwagę że "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:
var foo = 10;
function bar() {} // Deklaracja funkcji, (FD - function declaration)
(function baz() {}); // function expression
console.log(
this.foo == foo, // true
window.bar == bar // true
);
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:
Spójrzmy ponownie na powyższy kod. Funkcja baz jest to "function expression" i nie jest zawarta w Variable Object. Gdy z zewnątrz staramy się uruchomić funkcje 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, wiec 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 należącego do wywołującego (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).
Gdy funkcja lub metoda jest aktywowana (called) przez aktywującą funkcję lub metodę (caller), wtedy jest tworzony specjalny obiekt zwany Activation object . Jest on wypełniony parametrami formalnymi oraz dodatkowo posiada specjalny obiekt arguments, który to mapuje parametry formalne w porządku indeks-właściwości (index-properties). Po operacji utworzenia Activation object jest używany tak jak Variable object w kontekście funkcji.
Rozpatrzmy poniższy przykład:
function foo(x, y) {
var z = 30;
function bar() {} // Deklaracja funkcji (FD - function declaration)
(function baz() {}); 4. //FE - function expression
}
foo(10, 20);
W kontekście funkcji foo, powstał następujący activation object (AO):
Zauważmy że "functon expression" baz nie jest zawarta w variable/activation object.
Kompletny opis wraz z wszelkimi szczególnymi przypadkami (takimi 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 nadrzędnych (parent) lub do zmiennych w globalnym kontekście.
Jeżeli przyjmiemy Variable Object jako obiekt zasięgu w ramach pewnego kontekstu, to analogicznie do omawianego wyżej łańcucha prototypów, możemy utworzyć z tych obiektów ł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 kodzie kontekstu.
Zasada jest prosta i podobna do tej dla łańcucha prototypów: jeżeli zmienna nie jest 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ę gdy taka zmienna nie istnieje.
Jeżeli chodzi o konteksty to identyfikatorami są: nazwy zmiennych, deklaracje funkcji, formalne parametry itd. Czyli, jeżeli funkcja wskazuje we własnym kodzie identyfikator, który nie jest lokalną zmienną (a także lokalną funkcją lub parametrem formalnym), to taka zmienna jest nazywana zmienną swobodną. Do wyszukiwania tych zmiennych swobodnych wykorzystuje się łańcuch zasięgu (Scope chain).
Generalnie ujmując, łańcuch zasięgu (Scope chain) jest to lista składająca 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 funkcje 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).
var x = 10;
(function foo() {
var y = 20;
(function bar() {
var z = 30;
// "x" oraz "y" są "wolnymi zmiennymi"
// i są odnalezione (po aktywacji obiektu bar)
// w następnym obiekcie łańcucha zasięgu,
// który to (łańcuch) należy do obiektu bar.
console.log(x + y + z); // --> 60
})();
})();
Załóżmy że istnieją połączania obiektów wchodzących w skład łańcucha zasięgu, poprzez właściwość __parent__, która to wskazuje na następny element w ciągu obiektów/łańcuchu obiektów. 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 łańcucha zasięgu to prosta tablica. Używając pojęcia __parent__, możemy przedstawić powyższy przykład na rysunku znajdującym się poniżej (chociaż nadrzędny VO jest zapisany we właściwości funkcji [[Scope]]):
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, wiec mogą mieć prototypy oraz łańcuch prototypów. To dowodzi faktu iż wyszukiwanie w łańcuchu zasięgu jest dwuwymiarowe.
Dla naszego przykładu:
Object.prototype.x = 10;
var w = 20;
var y = 30;
// w SpiderMonkey obiekt globalny.
// Innymi słowy, VO (Variable object)
// dziedziczy z globalnego kontekstu "Object.prototype",
// tak wiec możemy odwoływać się do zmiennej "x", która
// "jest niezdefiniowaną zmienną globalną" (brak "var"),
// odnalezioną w łańcuchu prototypu.
console.log(x); // --> 10
(function foo() {
// "foo" lokalne zmienne
var w = 40;
var x = 100;
// "x" jest odnalezione
// w "Objekt.prototype"
// ponieważ {z: 50} z niego dziedziczy
with ({z: 50}) {
console.log(w, x, y , z); // --> 40, 10, 30, 50
}
// po tym jak obiekt "with" został usunięty
// z łańcucha zasięgu, "x" jest ponownie odnalezione
// w AO (Activation Object) kontekstu "foo";
// "w" jest także zmienna lokalna
console.log(x, w); // --> 100, 40
// oto jak możemy odwoływać się do
// przysłoniętej zmiennej "w"
// w środowisku przeglądarki
console.log(window.w); // --> 20
})();
Mamy wiec następująca 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__ ).
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 łańcuchu zasięgu 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 funkcja wewnętrzna może być zwrócona z funkcji nadrzędnej. Co więcej, ta zwrócona funkcja może być później aktywowana z innego kontekstu. Co się stanie z taką aktywacją, jeżeli kontekst jakiejś wolnej zmiennej już "nie istnieje"?
Generalnie 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).
W ECMAScript, funkcje są obiektami typu pierwszoklasowego (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 funargs - 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ą funkcyjna (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 maja rozwiązanie w ECMAScript, używając pokazanych na rysunku właściwości funkcji [[Scope]]).
Pierwszym 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 kontekście, 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ęgubędzie użyty do poszukiwania zmiennych przy okazji kolejnych wywołań tej funkcji.będzie użyty do poszukiwania zmiennych przy okazji kolejnych wywołań tej funkcji.
function foo() {
var x = 10;
return function bar() {
console.log(x);
};
}
// "foo" zwraca także funkcje
// i ta zwrócona funkcja
// używa wolnej zmiennej "x"
var returnedFunction = foo();
// zmienna globalna "x"
var x = 20;
// wykonanie zwróconej funkcji
returnedFunction(); // --> 10, ale nie 20
Ten typ zakresu jest nazywany zakresem leksykalnym lub statycznym (lexical/static scope). Widzimy że zmiennax 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
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 wywolujacej - caller? Aby uniknąć takich dwuznaczności, utworzono domknięcia i użyto statycznego zasięgu:
// globalna "x"
var x = 10;
// globalna funkcja
function foo() {
console.log(x);
}
(function (funArg) {
// lokalna "x"
var x = 20;
// nie ma dwuznaczności ponieważponiewaz
// użyliśmy globalnej zmiennej "x",
// która to została zapisana statycznie
// w [[Scope]] funkcji foo,
// i nie użyliśmy "x" z zasięgu funkcji wołającej (caller)
// która aktywuje "funarg"
funArg(); // 10, ale nie 20
})(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ć kombinacje 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.
Inna 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 miedzy wszystkimi funkcjami, które to mają ten sam zasięg nadrzędny. Zmiany dokonane na zmiennej przez jedno z domknięć są dzielone miedzy wszystkie funkcje, które mają ten sam nadrzędny zasięg.
function baz() {
var x = 1;
return {
foo: function foo() { return ++x; },
bar: function bar() { return --x; }
};
}
var closures = baz();
console.log(
closures.foo(), // 2
closures.bar() // 1
);
Powyższy kod można opisać poniższy rysunkiem:
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 maja tą samą wartość licznika wewnątrz funkcji. Jest tak ponieważ wszystkie te funkcje maja ten sam [[Scope]] gdzie licznik pętli ma ostatnio zapisana wartość.
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = function () {
alert(k);
};
}
data[0](); // 3, ale nie 0
data[1](); // 3, ale nie 0
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:
var data = [];
for (var k = 0; k < 3; k++) {
data[k] = (function (x) {
return function () {
alert(x);
};
})(k); // przekazanie wartości "k"
}
// teraz jest poprawnie
data[0](); // --> 0
data[1](); // --> 1
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 pojecie wartościthis.
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ść this nigdy nie uczestniczy w procesie wyszukiwania identyfikatora. Wiec, podczas używania this w naszym kodzie, jego wartość jest pobierana bezpośrednioz 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órym jest prostą zmienna podobnie zarządzaną, ale może zostać zmieniony 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 zmienna/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:
var x = 10;
console.log(
x, // --> 10
this.x, // --> 10
window.x // --> 10
);
W przypadku kontekstu funkcji, wartość this w 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 rożną wartość od wywołującego (caller), zmienia this w zależności od różnych wywołań (różnych sposobów aktywacji):
// Kod funkcji"foo"
// nigdy się nie zmienia, ale wartość "this"
// zmienia się za każdym uruchomieniem
function foo() {
alert(this);
}
// Wołający (caller) aktywuje "foo" - wołanego (callee)
// i ustawia "this" dla wołanego (callee)
foo(); // "this" wskazuje na kontekst Objektu globalnego
foo.prototype.constructor(); // "this" wskazuje na kontekst foo.prototype
var bar = {
baz: foo
};
bar.baz(); // "this" wskazuje na kontekst "bar"
(bar.baz)(); // także "this" wskazuje na kontekst "bar"
(bar.baz = bar.baz)(); // ale tu "this" wskazuje na kontekst Obiektu globalnego
(bar.baz, bar.baz)(); // także "this" wskazuje na kontekst Obiektu globalnego
(false || bar.baz)(); // nadal "this" wskazuje na kontekst Obiektu globalnego
var otherFoo = bar.baz;
otherFoo(); // ponownie "this" wskazuje na kontekst Obiektu globalnego
Dla głębszego zrozumienia jak wartość this może się zmienić dla rożnych wywołań tej samej funkcji, proponuje przeczytać Chapter 3. This. Są tam bardziej szczegółowo opisane wyżej omówione przypadki.
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 kompletna książkę. Nie zostały poruszone dwa główne zagadnienia: funkcje oraz różnice między poszczególnymi typami funkcji. Na przykład deklaracje funkcji i function expression 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.
Powodzenia w nauce ECMAScript!
Oryginalny 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