Łańcuch zasiegu i marnowanie pamięci we Flashu MX

- Timothée Groleau -


Wstęp

Flash MX jest potężnym narzędziem, a co za tym idzie developerzy muszą zachować ostrożność przy posługiwaniu się nim. Twój skrypt może dać nieoczekiwane rezultaty lub poprawne, ale nie będziesz wiedział dlaczego. Istnieje kilka przypadków gdy wiele dzieje się za plecami Twojego skryptu.

Dziś spróbuję omówić łańcuch zasięgu i pokazać w jaki sposób może on prowadzić do marnowania pamięci we Flashu MX. Mimo że dokument ten rozpoczyna się niezwykle prosto, przeznaczony jest raczej dla ludzi posiadających dobrą wiedzę na temat ActionScriptu. Wiele znajdujących się tu skryptów może być użytych w JavieScript, ale żadnego z nich nie testowałem w przeglądarce, więc nie gwarantuję że będą one działać.

Artukuł ten nie wnosi nic szczególnie nowego. Jest raczej rodzajem podsumowania wątków które przeczytałem na liście FlashCoders' List na której głównymi użytkownikami są Tatsuo Kato, Casper Schuirink, Peter Hall, Ralf Bokelberg, Gregory Burtch jak również wielu innych.

Jeśli się nie mylę, osobą która zidentyfikowała pojęcie łańcucha zasięgu dla funkcji zagnieżdżonych jest Naohiko Ueno na Japońskiej liście mailingowej. Oto link do jego postu (aby czytać archiwum tej listy musisz zasubskrybować tę listę). Informacja ta została następnie przeniesiona na listę FlashCoders przez Tatsuo Kato.

Wiele z zamieszczonych tu przykładów zostało wziętych "żywcem" z różnych jego listów i jest on zarazem wielkim inspiratorem tego artykułu.

Małe przypomnienie

O zarządzaniu pamięcią

Prawdopodobnie zauważyłeś, że w ActionScripcie nigdy nie ma potrzeby alokować lub zwalniać pamięci. Jest tak dlatego, gdyż ActionScript jest językiem bardzo wysokiego poziomu i automatycznie zajmuje się pamięcią za Ciebie.

Bardzo popularnym terminem w zarządzaniu pamięcią jest "odśmiecacz" (ang. garbage collector). Gdy tworzysz zmienne i obiekty, flash automatycznie przydziela dla nich pamięć. Odśmiecanie jest procesem, który śledzi czy obiekty są nadal używane przez program czy nie. Jeśli wykryje on że pewien obiekt nie jest już używany, skasuje go i zwolni pamięć po nim, udostępniając cenne zasoby.

Same odśmiecacze stanowią osobny rozległy temat, który wykracza poza ramy tego dokumentu. W celu śledzenia które obiekty jeszcze żyją posługują się one takimi technikami jak "zliczanie referencji" oraz "zaznacz i zamieć". W tym dokumencie mamy zamiar pokazać, że obiekt pozostaje w pamięci tak długo jak istnieje przynajmniej jedna referencja do niego w programie głównym lub w jednym z innych obiektów będących cały czas w użyciu przez główny program.


O funkcjach

"(var) używany jest do deklarowania zmiennych lokalnych. Jeśli zadeklarujesz zmienne jako lokalne wewnątrz funkcji, będą one zdefiniowane tylko dla tej funkcji i wygasają wraz z zakończeniem działania funkcji."

To zdanie pochodzi ze słownika ActionScriptu i dotyczy słowa kluczowego var. Tak jak zostało określone powyżej, var służy do deklarowania zmiennych lokalnych wewnątrz funkcji i te zmienne lokalne zostaną usunięte gdy funkcja zakończy swe działanie. To co się dzieje, to fakt że odśmiecacz usuwa je, gdyż nie ma już do nich żadnych referencji w programie głównym. I bardzo dobrze!

Prosty przykład:

1
2
3
4
5
6

test = function() {
var a = 5;
trace("inside: " + a);
}
test(); // inside: 5
trace("outside: " + a); // outside:


Wynik:
inside: 5
outside:

Jestem pewien że ten wynik was nie dziwi.
A to następny, ciekawy przykład z funkcjami. Z wnętrza funkcji odwołujemy się do zmiennej spoza funkcji:

1
2
3
4
5

a = 5;
test = function() {
trace(a);
}
test(); // 5

Wynik:
5

Powiesz "To normalne!". Ale co tak naprawdę się stało ? Jak flash uzyskuje dostęp do zmiennej "a" gdy jest ona na zewnątrz ciała funkcji. Odpowiedź brzmi: Flash podąża wzdłuż łańcucha zasięgu dociągniętego do funkcji.

Co to jest łańcuch zasięgu ?

Pojęcie "łańcuch zasięgu" pojawia się tylko w jednym miejscu w słowniku ActionScript. Na stronie dotyczącej akcji with. Każdemu polecam przeczytać tą stronę, gdyż jest ona pełna cennych informacji. Ponieważ jednak zawartość tej strony wybiega poza kwestie omówione do tej pory więc na razie nie będziemy jej omawiać.
Czym więc jest zasięg? Dla mnie zasięg jest obiektem do którego Flash zagląda w poszukiwaniu zmiennej. Opierając się na tej prostej definicji, możemy stwierdzić, że łańcuch zasięgu jest zestawem obiektów, do których Flash zagląda sekwencyjnie w poszukiwaniu zmiennej. Podczas wykonywania programu Flash widzi łańcuch zasięgu jako prosty stos, który zaczyna przeszukiwać począwszy od szczytu. Jeśli obiekt znajdujący się na szczycie nie zawiera poszukiwanej zmiennej, Flash przemieści się niżej do następnego obiektu i będzie powtarzał ten proces aż znajdzie zmienną lub zakończy przeszukiwanie całego łańcucha zasięgu.
Łańcuch zasięgu przeszukiwany jest w celu uzyskania zmiennej tylko wtedy, gdy nie podajemy jawnie jej zasięgu. Na przykład, gdy wykonujemy "trace(a);" Flash szuka "a" w łańcuchu zasięgu, gdyż nie podaliśmy jawnie gdzie "a" ma się znajdować. Gdy wykonujemy "trace(jakisObiekt.a);" to podajemy jawnie zasięg w którym mamy szukać a. Zasięg określony jest referencją jakisObiekt więc Flash będzie szukał zmiennej "a" tylko wewnątrz obiektu "jakisObiekt", a nie w łańcuchu zasięgu.
We Flashu kod ActionScript, może być zapisany tylko na listwie czasowej należącej do MovieClipu (będącym _root albo innym dowolnym). Z miejsca w którym znajduje się kod, łańcuch zasięgu składa się z co najmniej dwóch elementów: bieżącego obiektu, który zawiera kod i obiektu _global. Poniżej jest skrypt zawierający szybki test:

1
2
3
4
5

_global.a = 4;
a = 5;
trace(a); // 5
delete a;
trace(a); // 4

Wynik:
5
4

Opiszmy linia po linii co się tutaj dzieje. W linii pierwszej tworzona jest zmienna "a" wewnątrz obiektu _global. Linia 2 tworzy zmienną "a" o bieżącym zasięgu. W linii 3 aby wypisać zmienną "a" Flash poszukuje zmiennej "a" w bieżącym zasięgu, znajduje ją i wypisuje (5). Linia 4 usuwa "a" z bieżącego zasięgu. W linii 5 Flash aby wypisać zmienną "a" szuka jej w bieżącym zasięgu, ale jej nie znajduje. Przesuwa się zatem do następnego obiektu wzdłuż łańcucha zasięgu, tam znajduje "a" i wypisuje ją (4).
Powód dla którego pojęcie "łańcuch zasięgu" pojawia się na stronie dotyczącej akcji with jest taki, że with może być użyta aby dodać obiekt na szczyt łańcucha zasięgu.

1
2
3
4
5
6
7
8
9
10
11

_global.a = 4;
a = 5;
obj = new Object();
obj.a = 6;
with(obj) {
trace(a); // 6
delete a;
trace(a); // 5
delete a;
trace(a); // 4
}

Wynik:
6
5
4

Jak widać polecenie "trace(a)" wywoływane 3 razy, za każdym razem daje inny wynik. Jest tak dlatego, gdyż sukcesywnie usuwamy zmienną "a" z bieżącego zasięgu, więc Flash musi zaglądać coraz głębiej i głębiej w łańcuch zasięgu aby odnaleźć zmienną która pasuje do referencji "a".

Funkcja i obiekt aktywacji

Tworzenie funkcji

Gdy funkcja jest tworzona, to jako bieżącego łańcucha zasięgu, Flash używa łańcucha zasięgu dołączonego do funkcji jako jej własny łańcuch zasięgu. Łańcuch zasięgu nie ulega zmianie gdy zostanie dołączony do funkcji i nie mogą być do niego dodane ani z niego usunięte żadne obiekty. Na przykład:

1
2
3
4
5
6
7
8
9
10
11

a = 5;
test = function() {
trace(a); // 5
delete a;
trace(a); // undefined
};

obj = new Object();
obj.a = 6;
obj.meth = test;
obj.meth();

Wynik:
5
undefined

Pomimo tego, że "meth" jest teraz metodą obiektu "obj", to "obj" nie znajduje się w łańcuchu zasięgu "meth". Oznacza to, że łańcuch zasięgu funkcji "test", nie uległ zmianie po przypisaniu jej jako metody obiektu "obj". Jedyną rzeczą która może to odczuć z wnętrza funkcji jest referencja this (nie użyta w przykładzie powyżej). Więcej o referencji this będzie później.
Mógłbyś pomyśleć, że test przedstawiony powyżej nie był sprawiedliwy, bo funkcja została pierwotnie utworzona jako "test", ale to nie ma znaczenia. Jedyną istotną rzeczą jest to, że bardzo specyficzny kawałek kodu "function() {...}" pojawia się w bloku kodu. Nawet bez użycia tymczasowej zmiennej wynik pozostanie niezmieniony.

1
2
3
4
5
6
7
8
9

a = 5;
obj = new Object();
obj.a = 6;
obj.meth = function() {
trace(a); // 5
delete a;
trace(a); // undefined
};
obj.meth();

Wynik:
5
undefined

Wykonanie funkcji

Za każdym razem gdy wykonywana jest funkcja, w niewidoczny sposób tworzony jest obiekt. Obiekt ten przechowuje wszystkie zmienne lokalne zadeklarowane słowem kluczowym var, przekazane do funkcji parametry, oraz tablicę arguments. Obiekt ten nazywany jest obiektem aktywacji. Termin "obiekt aktywacji" także pojawia się tylko w jednym miejscu słownika ActionScript, i znowu ma to miejsce przy opisie with.
Gdy funkcja się wykonuje, jako bieżący łańcuch zasięgu używany jest łańcuch funkcji, a obiekt aktywacji znajduje się na jego szczycie. Zatem wewnątrz funkcji podczas wykonywania skryptu, łańcuch zasięgu wygląda następująco:
obiekt aktywacji -> łańcuch zasięgu funkcji
W powyższym kodzie, jeśli funkcja była utworzona w pierwszej klatce _root, to łańcuch zasięgu podczas jej wykonywania wygląda tak:
obiekt aktywacji -> _root -> _global
Jeśli chodzi o zarządzanie pamięcią, pojęcie obiektu aktywacji rzuca również nieco światła na to co dzieje się podczas wykonywania funkcji:

1. tworzony jest obiekt aktywacji
2. wszystkie zmienne lokalne tworzone są jako właściwości obiektu aktywacji
3. kod wykonywany jest w kontekście obiektu, którym jest obiekt aktywacji
4. gdy kod się kończy, obiekt aktywacji odkładany jest na stos śmieci i zasoby zostają zwolnione. Jest tak dlatego, bo nigdzie już nie ma odwołania do obiektu aktywacji.

Funkcje zagnieżdżone i marnowanie pamięci

Wstęp

Dotarliśmy wreszcie do interesującej części (przepraszam że to tyle zajęło). We flashu MX nowy model zdarzeń czyni niezwykle łatwym przypisywanie w locie funkcji do uchwytów zdarzeń (lub do innej zmiennej). Logiczną rzeczą która przychodzi do głowy jest utworzenie funkcji jako opakowania które ma za zadanie wykonanie konkretnej operacji ORAZ przypisywać różne funkcje do uchwytów zdarzeń
Na przykład:

1
2
3
4
5
6
7
8
9

resetMC = function(mc) {
mc._x = mc._y = 0;
mc.onEnterFrame = function() {
this._x += 2;
this._y += 2;
}
}
resetMC(mc1);
resetMC(mc2);

Funkcja "resetMC" pobiera movie clip jako parametr, resetuje jego położenie do (0,0) I przypisuje funkcję to uchwytu zdarzenia onEnterFrame co powoduje ruch klipu diagonalnie w kierunku prawego dolnego rogu. To brzmi rozsądnie, ale jest tu haczyk.

Trwały obiekt aktywacji

Zanim przejdziemy dalej, musimy wprowadzić nieco prostej terminologii, w przeciwnym razie, trudno będzie pisać o tym co się dzieje. Po prostu, gdy funkcja jest tworzona wewnątrz innej funkcji, funkcję pierwszą nazywamy wewnętrzną, a drugą zewnętrzną.
Jeśli więc zrozumiałeś wszystko co zostało napisane dotychczas, to wiesz pewnie o co chodzi. W powyższym kodzie za każdym razem gdy wywoływana jest funkcja "resetMC" tworzony jest obiekt aktywacji i dodawany jest on do łańcucha zasięgu funkcji, tworząc bieżący łańcuch zasięgu. Gdy zostanie osiągnięta linia 3, tworzona jest funkcja wewnętrzna i przypisywana jako uchwyt do zdarzenia onEnterFrame movie clipu przekazanego jako parametr.
Gdy utworzona jest funkcja wewnętrzna, bieżący łańcuch zasięgu zostanie przyporządkowany tej funkcji jako jej własny łańcuch zasięgu. Chodzi o to, że obiekt aktywacji funkcji zewnętrznej jest częścią bieżącego łańcucha zasięgu, co oznacza że w łańcuchu zasięgu funkcji wewnętrznej jest teraz trzymana referencja do niego.
Jeśli powyższy kod zostałby napisany w pierwszej klatce _root, łańcuch zasięgu powiązany z funkcją wewnętrzną wyglądałby tak:
obiekt aktywacji funkcji zewnętrznej -> _root -> _global
a gdy następnie funkcja zostanie wykonana łańcuch zasięgu będzie taki:
obiekt aktywacji funkcji wewnętrznej -> obiekt aktywacji funkcji zewnętrznej -> _root -> _global
" No i co z tego?" zapytasz. To, że ponieważ referencja do obiektu aktywacji funkcji zewnętrznej jest częścią łańcucha zasięgu funkcji wewnętrznej, a funkcja wewnętrzna jako metoda movie clipu jest trwała, zatem sam obiekt aktywacji staje się obiektem trwałym. W rzeczy samej dla garbage collectora istnieje teraz referencja do obiektu aktywacji więc nie może on zostać usunięty. Z tego powodu, obiekt aktywacji wraz ze wszystkimi zmiennymi lokalnymi które zawiera, nie może zostać zwolniony z pamięci.

Duplikacja funkcji

Jest kilka konsekwencji tworzenia funkcji wewnątrz funkcji.
Po pierwsze, jak wspomnieliśmy wcześniej za każdym razem gdy wykonywana jest funkcja, tworzony jest nowy obiekt aktywacji. Zatem, nawiązując do poprzedniego przykładu, wyobraź sobie że chcemy 100 razy wywołać funkcję resetMC w celu przypisania uchwytu onEnterFrame do 100 movie clipów. Czyniąc to w ten sposób spowodujemy utworzenie 100 różnych obiektów aktywacji, które będą zajmowały pamięć.
Po drugie, ponieważ funkcja wewnętrzna jest tworzona i przypisywana w funkcji zewnętrznej, to każdy movie clip będzie miał przypisany różny obiekt funkcyjny, każdy zajmujący pewien obszar pamięci. Jest to nadzwyczaj nieefektywne rozwiązanie, bo funkcja wewnętrzna robi to samo z każdym movie clipem to którego została przypisana (kod jest taki sam). Przy okazji, to także powód dla którego OOP zaleca stosowanie metod klas dostępnych w prototypie zamiast tworzenia ich w konstruktorze. Gdyby były one tworzone w konstruktorze, mielibyśmy duplikację funkcji dla każdej instancji klasy.

Marnowanie pamięci, nie wyciek pamięci

Rozróżnienie tych dwóch pojęć jest dosyć istotne. Na wyciek pamięci natrafiamy gdy program stale zwiększa zasoby z których korzysta, prowadząc czasem do zawieszenia systemu.
W przypadku zagnieżdżonych funkcji we flashu nie mamy wycieku pamięci, ale marnowanie pamięci. Funkcja wewnętrzna przechowuje referencję do obiektu aktywacji funkcji zewnętrznej, ale jest to relacja jeden-do-jednego. Jeżeli wewnętrzna funkcja zostanie usunięta z pamięci (na przykład gdy obiekt do którego została przypisana funkcja wewnętrzna został usunięty), to unikatowa referencja do obiektu aktywacji jest również kasowana i garbage collector skasuje zarazem (powinien skasować) funkcję wewnętrzną jak i obiekt aktywacji funkcji zewnętrznej. W ten sposób zasoby użyte na przechowanie obiektu aktywacji funkcji zewnętrznej zostały zmarnowane, ale się nie zwiększają, a co za tym idzie użycie pamięci nie wzrasta w sposób niekontrolowany.

Czy możemy to udowodnić?

O trwałości obiektu aktywacji

Oczywiście! Rozważ ten kawałek kodu:

1
2
3
4
5
6
7
8
9

test = function(obj) {
var a = 5;
obj.meth = function() {
trace(a);
}
}
o = new Object();
test(o);
o.meth(); // 5

W funkcji "test", "a" jest zmienną lokalną, więc możemy oczekiwać, że po wywołaniu funkcji "test", "a" jest niszczone, a pamięć zwolniona. Jednakże, gdy uruchomimy powyższy kod wywołanie "meth" na obiekcie "o" da wynik "5", co oznacza, że "a" ZOSTAŁO znalezione w łańcuchu zasięgu "meth" i nie zostało zwolnione z pamięci.
Mógłbyś pomyśleć, że "a" zostało odnalezione, bo w funkcji wewnętrznej była jawnie użyta referencja do niej. Flash mógł wykonać "coś" wewnętrznie aby utrzymać przy życiu tę referencję. Było by miło, ale w rzeczywistości nie w tym rzecz. Obiekt aktywacji funkcji zewnętrznej BYŁ przechowany w łańcuchu zasięgo funkcji wewnętrznej i dlatego mamy z niej dostęp do wszystkich zmiennych lokalnych.
Używając eval, możemy dynamicznie poszukać zmiennej w łańcuchu zasięgu. Popatrz na ten kod:

1
2
3
4
5
6
7
8
9
10
11
12
13

test = function(obj) {
var aVariable_1 = "Hello";
var aVariable_2 = "There";
var aVariable_3 = "Tim";
obj.retrieve = function(refName) {
trace(eval(refName));
}
}
o = new Object();
test(o);
o.retrieve("aVariable_1"); // Hello
o.retrieve("aVariable_2"); // There
o.retrieve("aVariable_3"); // Tim

W powyższym kodzie, funkcja "retrieve" nie zawiera jawnie wpisanej referencji do żadnej zmiennej, ale przekazując nazwę zmiennej i używając eval, możemy uzyskać dostęp do wszystkich zmiennych lokalnych, o których myśleliśmy że zostały skasowane. Mimo że zmienne te nie były używane w programie, to pozostawały w pamięci, marnując zasoby.

O duplikacji i marnowaniu pamięci

I znów! Rozważmy taki kod:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

addFunc = function(obj) {
var aVariable = new Object();
aVariable.txt = "Hello there";

obj.theFunc = function() {
return aVariable;
}
}

o1 = new Object();
o2 = new Object();

addFunc(o1);
addFunc(o2);

trace(o1.theFunc().txt); // Hello there
trace(o2.theFunc().txt); // Hello there

trace(o1.theFunc == o2.theFunc) ; // false
trace(o1.theFunc() == o2.theFunc()) ; // false

Linia 19 pokazuje, że pomimo tej samej nazwy, metody obiektów "o1" i "o2" nie odnoszą się do tego samego obiektu funkcji w pamięci.
Linia 20 pokazuje, że pomimo tego, że dla obu obiektów "o1" i "o2" zwracany przez "theFunc" obiekt ma tą samą wartość właściwości "txt", to same zwracane obiekty różnią się, co oznacza, że łańcuch "hello there" jest przechowywany w pamięci w dwóch miejscach, po jednym dla każdego obiektu.
Zatem w rzeczywistości za każdym gdy wywołujemy "addFunc" w celu dodania metody "theFunc" do obiektu, łańcych "Hello there" jest duplikowany w pamięci.

Jak temu zaradzić

Jeśli szczególnie zainteresowała cię wspomniana wyżej cecha funkcji zagnieżdżonych, to najlepszym rozwiązaniem jest utworzenie wszystkich twoich funkcji na jednych poziomie, a następnie zamiast zagnieżdżania używać wewnątrz funkcji jedynie referencji do nich.
Na przykład jeśli przepiszemy powyższy kod, będzie to wyglądało tak:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

theFunc = function() {
return aVariable;
}
addFunc = function(obj) {
var aVariable = new Object();
aVariable.txt = "Hello there";

obj.theFunc = theFunc;
}

o1 = new Object();
o2 = new Object();

addFunc(o1);
addFunc(o2);

trace(o1.theFunc().txt); // undefined
trace(o2.theFunc().txt); // undefined

trace(o1.theFunc == o2.theFunc) ; // true

Jak widać z powyższego kodu, polecenia trace w liniach 17 i 18 pokazują undefined. To dlatego, bo gdy wywoływana jest "theFunc" na obiektach o1 i o2, Flash nie może znaleźć referencji do "aVariable" w łańcuchu zasięgu, co oznacza, że obiekt aktywacji funkcji "addFunc" nie został dodany do łańcucha zasięgu metod i zmienna lokalna "aVariable" została usunięta tak jak powinniśmy tego oczekiwać.
Co więcej, linia 20 wypisuje teraz "true", co oznacza, że obiekt funkcji zarówno dla obiektu "o1" oraz "o2" jest jednakowy i zajmuje to samo miejsce w pamięci.
Powyższe podejście przyspieszy również działanie kodu jeśli chodzi o pamięć. Zasadniczo utworzenie nowej funkcji w pamięci zajmuje chwilę czasu: czas na przydział pamięci, czas na transfer danych itd. Gdy więc używasz funkcji zagnieżdzonych, to z każdym wywołaniem funkcji zewnętrznej zajmujesz kilka cykli procesora na zbudowanie funkcji wewnętrznej. Tworzenie funkcji z góry a następnie wykonanie prostego przypisania w funkcji zewnętrznej sprawi że wykonanie kodu zajmie mniej czasu.

Wnioski

Oto podsumowanie rozdziału poświęconego łańcuchowi zasięgu i marnotrawieniu pamięci. Reasumując wypunktujmy to co do tej pory zostało powiedziane:

1. Łańcuch zasięgu to sekwencja obiektów, które Flash przeszukuje w poszukiwaniu zmiennej.
2. Gdy funkcja jest tworzona, związywany jest z nią bieżący łańcuch zasięgu.
3. Za każdym razem gdy funkcja jest wykonywana tworzony jest nowy obiekt, zwany obiektem aktywacji, który przechowuje wszystkie zmienne lokalne, a następnie jest on umieszczany na szczycie związanego z funkcją łańcucha zasięgu.
4. Gdy zagnieżdżamy funkcje, obiekt aktywacji funkcji zewnętrznej zostaje umieszczony w łańcuchu zasięgu funkcji wewnętrznej.
5. Jeśli funkcja zostaje powiązana do obiektu trwałego jako jego metoda lub jest zwracana przez funkcję zewnętrzną, to wtedy funkcja wewnętrzna staje się trwała, a wraz z nią obiekt aktywacji funkcji zewnętrznej. To prowadzi do marnotrawienia pamięci.
6. Aby uniknąć marnowania pamięci, prostym rozwiązaniem jest nie używanie funkcji zagnieżdżonych, ale tworzenie funkcji zewnętrznie, a następnie przypisywanie referencji do nich.

To wszystko jest rzeczywiści proste. Problem w tym że wcale nie jest oczywiste. Zapewne wiele razy marnowałeś pamięć nie wiedząc nawet o tym. Zanim uciekniesz aby modyfikować swoje skrypty w których użyłeś funkcji zagnieżdżonych, chciałbym zwrócić uwagę, że w większości przypadków to wszystko nie ma wielkiego znaczenia. W małych projektach mało prawdopodobne jest że wyczerpiesz wszystkie zasoby maszyny i jeśli projekt sprawuje się dobrze, po prostu się tym nie martw. To o czym tu napisałem ma znaczenie przy wielkich projektach a pomyślałem że dobrze jest wiedzieć co Flash robi za Twoimi plecami. Myślę że największe marnotrawienie pamięci ma miejsce przy operowaniu dużymi łańcuchami tekstu i funkcjami zagnieżdżonymi. Łańcuch może osiągać długość kilkudziesięciu lub nawet kilkuset znaków. Jeśli więc używasz długich łańcuchów jako zmiennych lokalnych i stają się one trwałe, to możesz mieć poważny problem. Jeśli jest to kilka liczb całkowitych lub referencji, to nie powinno się stać nic złego.
Tylko nie każ mi mówić tego o czym nie powiedziałem, bo jeśli o mnie chodzi to im czystszy plik w sensie rozmiaru, zużycia pamięci i wydajności, tym lepiej. Zatem pamiętaj o tym gdy piszesz kody w ActionScripcie :).
Dziękuje że dotarłeś aż do tego momentu. Mam nadzieję że to, co tu przeczytałeś przyda Ci się. Jeśli zauważyłeś jakąś nieścisłość czy błąd (kod, fakty, terminologię, gramatykę, pisownię itd.) lub jeśli uważasz że inne przykłady objaśniające temat byłyby lepsze lub jeśli po prostu chciałbyś podzielić się innymi informacjami nie krępuj się napisać (flash@timotheegroleau.com). Z radością przeczytam każdy post.
Zanim przejdę dalej, pomyślałem że ten rozdział mógł nasunąć kilka pytań więc poniżej znajduje się sekcja "dodatki".
I ostatnia notka: cecha zawsze staje się źródłem eksperymentów. Funkcje zagnieżdżone mogą prowadzić to marnowania pamięci, ale jest to korzystne w niektórych aplikacjach. Na przykład prywatne i statyczne właściwości mogą być zaimplementowane używając cechy łańcucha zasięgu. Więcej na ten temat tu.

Odnośniki

Większość tego co znajduje się tutaj zostało zaczerpnięte z różnych wątków listy FlashCoders. Najciekawszym wątkiem według mnie jest:
http://chattyfig.figleaf.com/ezmlm/ezmlm-cgi?1:sss:56601:200212:blejmgjoemfcdojimbmn#b

Dodatki - pytania/odpowiedzi

Jak głęboko może sięgać łańcuch zasięgu?

Nie mam zielonego pojęcia. Próbowałem zagnieździć 5 poziomów funkcji i dla nich wszystkich obiekt aktywacji był trwały.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

a1 = 5;
addFunc = function(obj) {
var a2 = 6;
var func = function(obj) {
var a3 = 7;
var func = function(obj) {
var a4 = 8;
var func = function(obj) {
var a5 = 9;
obj.retrieve = function(refName) {
var a6 = 10;
trace(eval(refName));
}
}
func(obj);
}
func(obj);
}
func(obj);
}
o = {};
addFunc(o);
o.retrieve("a6"); // 10
o.retrieve("a5"); // 9
o.retrieve("a4"); // 8
o.retrieve("a3"); // 7
o.retrieve("a2"); // 6
o.retrieve("a1"); // 5

Ponieważ możemy odczytać wszystkie zmienne oznacza to, że obiekt aktywacji każdej funkcji znajdował się w łańcuchu zasięgu funkcji najbardziej wewnętrznej.
Tak więc, naprawdę nie wiem jak głęboko może sięgać łańcuch zasięgu, ale prawdopodobnie dosyć głęboko. W każdym razie powyższy kod ma 5 poziomów zagnieżdżenia i jeśli twój kod w rzeczywistym projekcie wygląda tak jak ten, to myślę że powinieneś poważnie jeszcze raz przeanalizować swoją strategię kodowania :).

Łańcuch zasięgu i łańcuch prototypów

Wspomnieliśmy wcześniej że gdy Flash szuka zmiennej robi to wzdłuż łańcucha zasięgu. To prawda, a na jego szczycie istnieje jeszcze jeden mechanizm, którego Flash używa poszukując zmiennej. Ten mechanizm to łańcuch dziedziczenia znany także jako łańcuch prototypów. Ponieważ ten artykuł nie jest o OOP, nie chcę się za bardzo wgłębiać więc odeślę Cię do książki online Roberta Debreuil'a, która jest najlepszym źródłem wiedzy na temat OOP we Flashu.
W skrócie, każdy obiekt we Flashu jest instancją pewnej klasy, która to może dziedziczyć po jeszcze innej klasie i tak dalej. Każda klasa może mieć swój własny zestaw właściwości i metod. Kiedy wywołujesz metodę na obiekcie, to jeśli metoda nie jest zdefiniowana dla tego właśnie obiektu to Flash będzie przeszukiwał łańcuch dziedziczenia aby znaleźć szukaną metodę wyżej w hierarchii klas.
Pomimo, że łańcuch zasięgu używany jest wewnętrznie, to łańcuch dziedziczenia jest dostępny dla programistów, a to co łączy obiekt z łańcuchem prototypów jest referencja "__proto__". Gdy ma miejsce przeszukiwanie łańcucha zasięgu, uruchamiane jest także przeszukiwanie łańcucha prototypów dla każdego obiektu w łańcuchu zasięgu.

Ilustruje to poniższy kod:

1
2
3
4
5
6
7
8
9
10
11

a = 5;
addFunc = function(obj) {
var __proto__ = new Object();
__proto__.a = 6;
obj.meth = function() {
trace(a);
}
}
o = {};
addFunc(o);
o.meth(); // 6

Wartość "6" nie została przypisana zmiennej "a" tak jak ma to miejsce w przypadku gdy jest ona zmienną lokalną wewnątrz funkcji "addFunc". Mimo to została ona wyświetlona jako wartość poszukiwanej zmiennej "a". Po kolei omówmy więc, co Flash zrobił gdy wykonywana była metoda "meth"?
Na początku Flash szukał "a" w obiekcie aktywacji "meth" ale jej tam nie znalazł, następnie sprawdził czy obiekt aktywacji metody "meth" ma referencję __proto__. Ponieważ jej nie ma (albo jej nie widzimy), Flash poszedł dalej przeszukując następny obiekt w łańcuchu zasięgu którym jest obiekt aktywacji funkcji "addFunc". Tam też jej nie znalazł. Flash więc sprawdził czy istnieje referencja __proto__ tego obiektu; okazało się że istnieje. Flash zajrzał czy obiekt wskazywany przez referencję __proto__ zawiera "a" i ją tam znalazł, po czym ją wyświetlił.

Jak przypisywane są zmienne bez jawnie podanego zasięgu ?

Nie wiem czy powinienem o tym mówić na samym początku. Po raz kolejny spotykamy właściwość która nie jest skomplikowana, ale nie jest też oczywista. Gdy tworzysz przypisanie "myVar=5", każdy z obiektów łańcucha zasięgu jest przeszukiwany w celu znalezienia "myVar" POZA _global. Jeżeli choć jeden z tych obiektów (nazwijmy go "o") posiada właściwość "myVar" to przypisanie zostanie dokonane na "o". Jeśli żaden z nich nie posiada tej właściwości, to tworzona jest zmienna "myVar" dla najniżej położonego obiektu w hierarchii łańcucha zasięgu. Ten obiekt będzie się znajdował zaraz nad _global.
Najwidoczniej w przypadku przypisania łańcuch prototypów nie jest przeszukiwany, więc nawet jeśli "myVar" istnieje gdzieś w łańcuchu prototypów obiektu "o", to przypisanie nie będzie wykonane na "o" jeśli nie jest on ostatnim obiektem w łańcuchu zasięgu tuż nad _global.
Aby stworzyć albo ustawić właściwość obiektu _global, _global musi zostać podane jawnie.
Poniżej znajduje się kilka skryptów testowych, które to ilustrują:

1
2
3
4
5
6
7
8
9
10
11

o1 = {};
o2 = {};
with (o1) {
with (o2) {
trace("The first 'a' reference found is: " + a);
a = 5;
}
}
trace(o1.a); // undefined
trace(o2.a); // undefined
trace(a); // 5


1
2
3
4
5
6
7
8
9
10
11

o1 = {a:4};
o2 = {};
with (o1) {
with (o2) {
trace("The first 'a' reference found is: " + a);
a = 5;
}
}
trace(o1.a); // 5
trace(o2.a); // undefined
trace(a); // undefined


1
2
3
4
5
6
7
8
9
10
11
12

o1 = {};
o1.__proto__ = {a:4};
o2 = {};
with (o1) {
with (o2) {
trace("The first 'a' reference found is: " + a);
a = 5;
}
}
trace(o1.a); // 4
trace(o2.a); // undefined
trace(a); // 5

With i łańcuch zasięgu

Kocham "with" i nienawidzę "with". To dlatego że ponieważ ma wpływ na łańcuch zasięgu upodabnia go do funkcji zagnieżdżonych. Na przykład to o czym mówiliśmy powyżej na temat przypisania jest prawdą zarówna dla funkcji zagnieżdżonych jak i dla "with". Jest jednak kilka różnic. Największa o której chcę powiedzieć jest związana z tworzeniem funkcji. Wspominałem wcześniej w tym artykule że do funkcji dociągany jest bieżący łańcuch zasięgu gdy jest ona tworzona. Jak widzieliśmy powoduje to utrwalenie obiektu aktywacji. Jeśli do funkcji jest dociągany obiekt aktywacji w momencie jej tworzenia, to oznacza to że możemy użyć "with" w celu dodania obiektu do łańcucha zasięgu funkcji. To jednak nie działa, spójrz na ten przykład:

1
2
3
4
5
6
7
8
9

o1 = {a:5};
o2 = {};
with (o1) {
trace(a); // 5
o2.aMethod = function() {
trace(a);
};
}
o2.aMethod(); // undefined

W powyższym kodzie użyliśmy "with" w celu umieszczenia obiektu "o1" na szczycie łańcucha zasięgu. Linia 4 pokazuje że możemy odnieść się do "a" tak jak powinno być. Mając bieżący łańcuch zasięgu tworzymy funkcję (linia 5) i przypisujemy ją do "o2" jako jego metodę. Potem wywołujemy "o2.aMethod();", co zwraca nam undefined. To oznacza, że "a" nie zostało znalezione w łańcuchu zasięgu i oznacza że o1 nie zostało dodane do łańcuchu zasięgu funkcji. Obiekt ręcznie wstawiony do łańcucha zasięgu za pomocą "with" NIE JEST uważany za część łańcucha zasięgu dociąganego do funkcji w czasie jej tworzenia.

Co z referencją „this” ?

Na ile udało mi się stwierdzić łańcuch zasięgu nie ma wpływu na "this" bez względu na głębokość. "this" zawsze wskazuje na obiekt na którego rzecz wywoływana jest funkcja.
Inaczej mówiąc, gdy łańcuch zasięgu funkcji nigdy się nie zmienia, to znaczenie "this" wewnątrz funkcji zależy od tego na jakim obiekcie wywoływana jest ta funkcja. Jeśli wrócimy do jednego z pierwszych przykładów:

1
2
3
4
5
6
7
8
9
10
11
12

a = 5;
test = function() {
trace(this.a);
};
obj = new Object();
obj.a = 6;
obj.meth = test;
obj2 = new Object();
obj2.a = 7;
obj2.meth = test;
obj.meth(); // 6
obj2.meth(); // 7

Stwierdzenie, że "'this' zawsze odnosi się do obiektu z którego funkcja została wywołana", prowadzi do interesującej cechy. Wspomnieliśmy wcześniej że zmienne lokalne są po prostu właściwościami obiektu aktywacji. Jeśli używając var zdefiniujemy funkcję dla obiektu aktywacji i uruchomimy ją z wnętrza obiektu aktywacji to "this" wewnątrz tej funkcji będzie wskazywało właśnie na obiekt aktywacji! Wykorzystując ten fakt możemy uzyskać referencję do obiektu aktywacji i ją sobie gdzieś przechować (jeśli widzisz z tego jakiś użytek) lub użyć operatora tablicy aby utworzyć lub odczytać jakąś właściwość obiektu aktywacji (znów, jeśli coś takiego Ci się przyda).

1
2
3
4
5
6
7
8
9

a = 4;
test = function() {
var a = 5;
var getRef = function() {
trace(this.a);
}
getRef(); // 5
}
test();


Operator tablicy i eval

Ludzie zawsze się zastanawiają czy eval w actionscripcie wychodzi z użycia i jest niezalecane czy nie i jaka jest różnica pomiędzy eval a operatorem tablicy. Cytat wypowiedzi Ralpha Bokelberga dostarcza odpowiedzi na to pytanie tutaj - http://chattyfig.figleaf.com/ezmlm/ezmlm-cgi/1/67735 - cytat poniżej:
<quote>
Do you know the mantra of eval:

eval is not deprecated
eval is not useless
eval is just fine to use

how else would you access objects given by a targetstring

obj = {subobj: {prop: 666}}
path = "obj.subobj.prop";
trace(this[path]); //undefined
trace(eval(path)); //666

bokel
</quote>
Więc rzeczywiście jedną z głównych różnic jest to że eval ma dostęp do właściwości obiektu mając daną ścieżkę, natomiast operator tablicy ma dostęp jedynie bezpośredni.
Stosując to co zostało omówione, możemy pokazać różnicę pomiędzy eval i operatorem tablicy wykazując ich związek z łańcuchem zasięgu i łańcuchem prototypów. Zakładając że do pobrania pojedynczej właściwości (bez ścieżki) używamy eval lub operatora tablicy możemy powiedzieć: operator tablicy służy do pobrania zmiennej (właściwości) przeszukując łańcuch prototypów danego obiektu; eval jest używane do wydobycia zmiennej przeszukując bieżący łańcuch zasięgu. I, jak wspomnieliśmy wyżej przeszukiwanie łańcucha zasięgu powoduje przeszukanie łańcucha prototypów dla każdego obiektu w łańcuchu zasięgu.

Czy wszystko zrozumiałeś ?

Dobra, to już naprawdę koniec. Korzystając z wiedzy zdobytej w tym artykule i nie używając Flasha MX czy możesz powiedzieć co zostanie wyświetlone jako wynik działania tego kodu ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

getMethod = function() {
var setProto = function() {
this.__proto__ = o1;
};
setProto();

return function() {
trace(a);
}
}

_global.a = 4;
o1 = {a:5};
o2 = {a:6};
a = 7;

o2.theMethod = getMethod();
o2.theMethod();

a. 4
b. 5
c. 6
d. 7