这是最新一篇日志上一篇 | 下一篇
AcionScript 订阅所有AcionScript的日志

Flash MX 中的範圍鏈 (scope chain)與記憶體浪費



 

Flash MX 中的範圍鏈 (scope chain)與記憶體浪費

Timothée Groleau著

李易修 譯

原文刊載於http://timotheegroleau.com/Flash/articles/scope_chain.htm

首次發表於 8 April 2003

 

簡介

Flash MX是非常強大的工具,伴隨而來的是開發者必須注意他們做了什麼。你寫的程式碼可能有無法預期的結果或運作方式,但是你可能不知道為什麼會有這樣的情況發生。這些情況有一大部分是發生於你的程式碼的背面。

所以今天,我將介紹範圍鏈(scope chain),並呈現在Flash MX中範圍鏈如何導致記憶體的浪費。雖然這篇文章前半段非常基礎,但是讀者最好能夠對actionscript有比較深入的了解。

這邊提到的部分程式碼或許能夠應用到JavaScript,但是我並未測試過,不能擔保一定能夠運作。 

誌謝

這篇文章並沒有新的創見,只能算是Tatsuo Kato, Casper Schuirink, Peter Hall, Ralf Bokelberg, Gregory Burtch等人在FlashCoders' List討論內容的摘要。

具我所知,巢狀函式 (nested function)的範圍鏈是 Naohiko Ueno在日文電子論壇上先提出的。你可以到這裡去看他所寫的文章。(注意:你必須加入這個論壇,才能閱讀這些文章)這個資訊是Tatsuo Kato在FlashCoders' list上提供的。

這篇文章裡面的許多範例取自Tatsuo Kato先前的文章和巧妙的概念。

回顧

關於記憶體管理

你或許已經注意到,在actionscript中你從來不需要分配 (allocate)或釋放記憶體。因為actionscript是高階程式語言,能夠自動幫你處理記憶體的問題。

在記憶體管理中的垃圾收集器 (garbage collector)一個非常熱門的議題。當你建立了變數與物件,Flash自動配置記憶體給它們。垃圾收集器是一個程式,它會持續追蹤物件是否持續被你的程式使用。如果它偵測到物件不在被使用,它會刪除這個物件並釋放記憶體,釋放先前使用的資源。

垃圾收集器是一個完整的議題,已經超出這篇文章的範圍。它使用類似參照次數 (reference coun)t與標記清掃 (mark and sweep)等技術來追蹤物件是否續存。這篇文章當中,我們將會提到主程式裡至少有一個參照或有任何一個物件在使用時,物件就會存在記憶體當中。

 

關於函式

(var)用來宣告區域變數 ( local variables)。如果你在函式裡宣告區域變數,變數是定義給函式使用,當呼叫函式結束後變數即失效。」

這個敘述是摘自 actionscript dictionnary內對var的說明。就像上面所說的:var被用來宣告函式內的區域變數,當函數的執行結束後,區域變數將會被刪除。這個時候垃圾收集器會移除區域變數,因為它們在主程式內已經沒有參照。

下面是一個簡單的例子:

程式碼:
1
                        2
                        3
                        4
                        5
                        6
                        
test = function() {
                        var a = 5;
                        trace("inside: " + a);
                        }
                        test(); // inside: 5
                        trace("outside: " + a); // outside: 
輸出:

inside: 5
outside:

我確定你已經很熟悉上面這種情況了。 

關於函式,這裡有一個有趣的例子。你可以從函式外部呼叫函式內部的變數,像下面這樣:

程式碼:
1
                        2
                        3
                        4
                        5
                        
a = 5;
                        test = function() {
                        trace(a);
                        }
                        test(); // 5
輸出:

5

你說會說:「這有什麼大不了的?」但實際上發生了什麼事?在函式外面時,Flash如何存取變數"a"呢?答案是Flash依尋著依附在函式上的範圍鏈來存取變數。

 

什麼是範圍鏈 (scope chain)?

範圍鏈只在actionscript字典中說明"with"指令的部分出現過一次。我建議每個人都讀一讀那一頁,因為可以從那邊取得許多寶貴的資訊。對於本文來說,那一頁講的太快了,所以讓我們慢下來,讓我好好的做個說明。

什麼是範圍 (scope)?對我來說,範圍是Flash尋找變數時的物件。簡單的說,範圍鏈是當Flash尋找變數時,依序檢查的一組物件。實際執行時,Flash把範圍鏈視為堆疊 (stack),從上開始向下搜尋。當上方範圍鏈上方的物件內找不到欲尋找的變數時,Flash會移動到下個物件,重複同樣的動作,直到找到變數,或整個範圍鏈都已經被檢查過。

當未明確指出範圍時,範圍鏈是唯一取得屬性的一種方式。舉例來說,當執行"trace(a);"時,因為並未明確指定"a"的位置,Flash在範圍鏈內尋找"a"。當我們執行"trace(anyObject.a)"時,就是指定了尋找"a"的詳細位置。"anyObject"參照具體指定了範圍,因此Flash將在物件"anyObject"內部尋找"a",而不到範圍鏈內尋找。

在Flash當中,actionscript只能寫在時間軸上,而時間軸是在影片片段 (movie clip)內(_root或其他的影片片段內)。從放置actionscript程式碼的位置開始,範圍鏈至少包含兩個元素:目前的物件(包含這個程式碼的物件)和_global物件。下面是一段測試的程式碼:

程式碼:
1
                        2
                        3
                        4
                        5
                        
_global.a = 4;
                        a = 5;
                        trace(a); // 5
                        delete a;
                        trace(a); // 4
輸出:

5
4

我們逐行解釋,這一行發生了什麼事:第1行在_global 物件中建立了變數"a"。第2行在目前的位置建立了變數"a"。第3行,為了追蹤a,Flash在目前的位置尋找"a",找到之後就輸出結果(5)。第4行刪除目前位置的"a"。在第5行,為了追蹤"a",Flash在目前的位置尋找"a",結果找不到"a",於是就開始到範圍鏈內的第二個物件尋找"a",找到之後輸出結果(4)。

範圍鏈出現在actionscript字典裡面with那一頁的原因,在於with能夠在範圍鏈的最上方新增一個物件。

程式碼:
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
                        }
輸出:

6
5
4

就如同你所看到的同樣的"trace(a)"敘述,被執行了三次,每次的輸出都不同。因為我們刪除了變數"a",因此Flash不斷向下搜尋範圍鏈內的與參照相同的變數。

 

函式與activation物件

函式的建立

當一個函式被建立、被Flash使用時,它的範圍鏈就依附在函式的範圍鏈。

依附在函式上時,範圍鏈就無法改變,無法從中新增或刪除新的物件。請看下面的例子:

程式碼:
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();
輸出:

5
undefined

雖然"meth"為物件"obj"的一個方法,但"obj"不在"meth"的範圍鏈中。也就是說"test"的範圍鏈無法被"obj"影響。唯一能夠影響函式內部的,就只有"this"參照(前面的例子並未使用)。關於"this"參照的說明,容後再述。

你或許說前面的測試並不公平,因為這個函式最初以test為名而建立。唯一要注意的是"function() {...}"這段程式碼在整個程式中出現的位置。當我們不使用暫時的變數時,結果依然相同:

程式碼:
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();
輸出:

5
undefined

 

執行函式

每當函式被執行,一個新的物件被建立。這個物件存有所有由關鍵字var所建立的的區域變數,包含函式的參數 (paramenter)、參數陣列 (arguments array)。這個物件稱作activation物件 (activation object)。activation物件只在actionscript字典內出現過一次,也是在with指令那一頁。

當函式執行時,函式建立的activation物件被放函式的範圍鏈的最上方。因此,當函式執行時,函式的範圍鏈為:

activation物件->函式的範圍鏈

在上面的程式碼中,如果函式被建立在_root的第1個影格,那麼當函式被執行時,範圍鏈為:

activation物件->_root->_global

以記憶體管理的角度來說,activation物件的概念闡明了當函式執行時發生了什麼事:

  1. activation物件被建立。
  2. 所有的區域變數被建立,成為activation物件的屬性。
  3. 程式碼執行,activation物件做為context物件。
  4. 當執行完畢,直到整個程式當中沒有和activation物件之間沒有任何連結之後,沿著所有的屬性,垃圾收集器將所占用的資源釋放。

 

巢狀函式與記憶體浪費

介紹

我們終於可以開始進入正題。使用Flash MX,新的事件模型 (event model)能夠方便的動態 (on the fly)指定函式到任何事件處理程式 (event handler)(或任何處理這件事的變數)。也就是說,函式能夠將程式碼包裝並且將其他函式指定到事件處理程式裡。

舉例來說:

程式碼:
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);

"resetMC"函式把影片片段當成參數,將位置重新設定為(0,0),並且把函式附加到onEnterFrame處理程式,因此影片能夠開始像右下角移動。這樣舉例應該夠清楚了。然而,這個部分值得深入探討。 

activation物件的持續存在

在繼續說明之前,我們先介紹幾個簡單的術語。不同的是,這個部分比較複雜。簡單來說,當一個函式被建立在另一個函式內時,我們稱裡面這個函式為內部函式 (inner function),外面的這個物件稱外部函式 (outer function)。

如果你了解我們先前所說的,你應該已經知道發生了什麼事。在上面的程式當中,每次"resetMC"被呼叫,activation物件被建立,並加到函式的範圍鏈。在第3行,內部函式被建立,被指定到傳入的影片片段的onEnterFrame處理程式中。

當內部函式被建立,目前的範圍鏈將會被附加到該函式做為它的範圍鏈。也就是說,外部函式的activation物件是目前的範圍鏈的一部分,代表一個參照已經被加到內部函式的範圍鏈上。

一般情況下,如果上面的程式被寫在_root的第1個 影格,附加到內部函式的範圍鏈應該是這樣的:

內部函式的activation物件 ->_root -> _global

當內部函式執行時,範圍鏈則變成:

內部函式的activation物件 -> 外部函式的activation物件 -> _root -> _global

你或許會問:「這有什麼大不了?」是的,因為外部函式的activation物件現在是內部函式的範圍鏈的一個參照,而內部函式現在是持續存在的,是影片片段的一個方法,所以activation物件也是持續存在的。的確,對垃圾收集器來說,activation物件有一個持續存在的參照,因此無法移除。因為上面的原因,activation物件內區域變數所佔用的記憶體無法被釋放。 

複製函式

有許多方法可以實做巢狀函式。首先,就像我們前面注意到的,當函式每次被執行就會建立一個新的activation物件。因此,依然根據前面的程式碼,如果我們使用"resetMC"函式做為100個影片片段的onEnterFrame處理程式,那麼我們將會有100個不同的activation物件持續存在記憶體當中。

第二,因為內部函式被建立,並被外部函式指定到每個影片片段的onEnterFrame處理程式,所以每個影片片段都被指定了不同的內部函式,每個處理程式都會佔用記憶體。很明顯的每個被onEnterFrame處理程式附加的影片片段,不能共用相同的內部函式並不是很有效率的處理方式。此外,物件導向程式建議在原型建立類別的方法,取代在建構式內建立方法。如果在建構式內建立方法,那麼每次多一個類別實體,就會複製一次相同的函式。 

記憶體浪費,不是記憶體漏失 (memory leak)

分辨這兩者的差異是很重要的。記憶體漏失是使用的資源不斷增加,可能造成系統當機的一種情況。

以Flash中的巢狀函式來說,並沒有記憶體漏失的問題,而是記憶體浪費。內部函式一直保有一個對外部函式的activation物件的參照,但是這是一個一對一的的關係。當內部函式被從記憶體中移除時(舉例來說,被附加內部函式的物件被移除),對activation的唯一的參照也被刪除,接下來垃圾收集器將會(應該)把內部函式和外部函式的activation物件同時刪除。所以儲存外部函式的activation物件所利用的資源就會被浪費,但不會一直增加下去,造成失控。

我們能夠印證嗎?

activation物件的持續存在

是的!看看下面的程式碼:

程式碼:
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
輸出:

5

在"test"函式中,"a"是一個區域變數,所以我們可以預期當我們呼叫"test"函式之後,"a"會消滅並從記憶體中釋放。然而,當我們執行上面的程式,呼叫物件"o"上的"meth"會輸出"5",這代表能夠在"meth"的範圍鏈當中找到"a",證實了"a"並未從記憶體中釋放。

你或許會以為找得到"a",因為在內部函式內有一個寫死的參照 (hardcoded reference)。Flash應該會做某件事情,確保特定的參照存在。如果是這樣確實不錯,但是實際上並非如此。外部函式的activation物件被存在內部函式的範圍鏈之內,而所有的區域變數都被存取。

使用eval,我們就可以動態的在範圍鏈內搜尋變數。讓我們看看下面這一段程式:

程式碼:
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
輸出:

Hello
There
Tim

上面的程式中,"retriece"函式內的所有變數都沒有被寫死的參照,尚未透過變數名稱或使用eval前,我們就能夠取得所有那些我們以為已經被刪除的區域變數。雖然這些變數並未被主程式使用,但是它們仍然存在記憶體當中,造成資源的浪費。

關於複製與記憶體浪費

是的,再一次!讓我們看看下面的程式碼:

程式碼:
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
輸出:

Hello there
Hello there
false
false

第19行顯示了雖然"o1"和"o2"的方法有這同樣的名稱,但是它們並不是參照到記憶體中的同一個函式。

第20行顯示雖然"o1"和"o2"物件藉由"theFunc"回傳的"txt"的屬性有相同的值,但是這些物件其實並不相同,代表"hello there"字串在記憶體中儲存了兩次,一個物件一次。

事實上,每次"addFunc"被呼叫,增加"theFunc"到某個物件上時,"Hello there"字串就在記憶體內被複製了。 

我們可以怎麼做?

除非你對上面提到的巢狀函式特別感興趣,否則最好的方式就是在同一個階層建立所有的函式,且並且只在函式內部使用函式參照,以取代巢狀迴圈。

舉例來說,如果我們重寫前面列出的程式,像下面這樣:

程式碼:
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
輸出:

undefined
undefined
true

就像你在前面所看到的,第17、18行輸出結果為undefined。這是因為,當"o1"和"o2"的"theFunc"被呼叫,Flash不能在範圍鏈內找到"aVariable",代表"addFunc"的activation物件沒有被加到這個方法的範圍鏈之內,且區域變數"aVariable"也如預期的被刪除了。

再者,第20行輸出的是"true",代表"o1"與"o2"的函式物件是相同的,只存了一份在記憶體當中。

記憶體空間為重要考量時,這個方法能後讓你的程式碼執行的更快速。基本上,這個方法不會把時間浪費在再記憶體中建立新的函式:記憶體配置、資料傳輸等等的時間。因此,當你使用巢狀函式時,每此你呼叫外部函式,你會使用較少的處理器循環來建立內部函式。先建立函式並且只在外部函式中設定簡單的指定,能夠讓執行效能大大的提升。

結論

結論包含範圍鏈與記憶體浪費。簡單來說,下列幾點是我們討論過的:

  1. 範圍鏈是當Flash尋找變數時檢查時的一連串物件。
  2. 當函式被建立時,目前的範圍鏈將依附在函式上。
  3. 每次函式被執行時,一個新的物件被建立,稱作activation物件。activation物件存有所有的區域變數,而此物件被放在函式所依附的範圍鏈的最上方。
  4. 如果是巢狀函式,外部函式所產生的activation物件會被放在內部函式所產生的範圍鏈內。
  5. 如果內部函式被附加在一個持續存在的物件上做為方法,或是從外部函式回傳,如此內部函式便會變成和外部函式的activation物件持續存在,造成記憶體的浪費。
  6. 為了避免記憶體的浪費,一個簡單的解決方式就是不要使用巢狀函式,而是在外部建立函式,只使用附加參照。

事實上,這件事十分單純。這個問題並不是很明顯。很多時候,你常常不知不覺中就浪費了系統資源。 

在你開始改寫你所有使用巢狀函式的程式碼之前,我想澄清一點,在大部分的情況下,使用巢狀迴圈沒有什麼大不了的。在小案子裡,如果你的程式並不會佔用所有的系統資源,而且執行效能良好,就不需要再去改寫它了。當你的程式比較複雜時,我還是建議你去了解一下Flash如何執行你的程式碼。

我想當你處理大量的文字資料與巢狀函式時,會造成大量記憶體的浪費。字串能夠很快的達到好幾打甚至幾百個字元,所以如果你在區域變數裡面儲存了很長的字串,並且持續存在時,你就可能遭遇到問題了,如果只有簡單的整數或參照,那麼情況不會太糟。

讀了本文之後,對於檔案的整理、檔案大小、記憶體的使用與效率,別太掛心。繼續寫你的程式,只要記得有這回事就好:)。

謝謝你讀到這邊。我希望這篇文章對你有幫助。如果你發現任何錯誤(程式碼、事實、術語、文法、拼字等等)或是你有更適合的例子,或只是想與我分享一些新東西,請寫信給我。得到迴響的話,我會很開心的。

在離開之前,我想這個議題會引發出一些其他的問題,所以我在文末加了附錄這個章節。

最後一句話:一個特色總是建立在大量的練習之上。巢狀函式可能會造成記憶體的浪費,對一些應用來說,這可能會派上用場。舉例來說,private和static屬性能夠利用範圍鏈來實做;關於這個議題,可以參考另一篇文章

參考資料

本文中大部分的概念來自欲FlashCoder's list中的討論串。最有趣的應該就是下面這篇:

http://chattyfig.figleaf.com/ezmlm/ezmlm-cgi?1:sss:56601:200212:blejmgjoemfcdojimbmn#b

 

附錄 - 問題與解答

範圍鏈能有多深?

恩,我不知道 :)。我寫了五層的巢狀函式,它們的activation物件都存在。

程式碼:
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
輸出:

10
9
8
7
6
5

既然我們能夠取的所有變數,那麼就表示每個函式的activation物件保留了最裡面的內部函式。

所以我也不知道範圍鏈能有多深,但是這數字肯定不小。但是在任何案子裡面,如果你已經用了五層的巢狀函式,我想你最好重新調整一下你的寫作策略!

範圍鏈與原型鏈 (prototype chain)

我們先前提到過當Flash搜尋變數時,會從範圍鏈最上端開始向下搜尋。這樣的機制稱為繼承鏈 (inheritance chain),而原型鏈也是另一種繼承鏈。本文雖然沒有討論OOP,但是如果想要了解Flash內如何實作OOP的話,可以參考Robin Debreuil的oline book

概括的說,Flash內的每個物件都是類別的實體,特性是由其他類別繼承而來的。每個類別可能有它自己的屬性和方法,當你呼叫物件內的方法時,如果物件本身沒有提供,Flash就會向上尋找繼承鏈,直到找到方法或是到達最上層。

雖然範圍鏈是屬於Flash內部的操作,但是原型鏈卻能夠直接給開發者利用,經由 "__proto__"參照連結各物件之間的已存在原型鏈。

下面是實作的方法:

程式碼:
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
輸出:

6

當Flash尋找變數"a"時,可以發現6未被指定為"addFunc"函式內區域變數"a"的值。那麼當"meth"被執行時,Flash做了什麼?

首先Flash在"meth"的activation物件內尋找"a",但是沒有找到。於是Flash尋找"meth"的activation物件是否有 "__proto__"參照。結果找不到 "__proto__"參照,因此Flash移動到範圍鏈的下一個物件,也就是"addFunc"的activation物件。在這邊還是沒找到"a"。於是,Flash尋找''__proto__"參照,找到了之後,在其中搜尋"a"。最後Flash在物件內的"__proto__"找到了"a",讀出它的值。

當沒有明確的範圍時,變數如何被設定?

我不知道是否應該先說明這個問題。這個問題並不複雜,但也不十分明顯。當你設定一個變數的值時,如"myVar=5;",Flash會到範圍鏈內的每個物件尋找是否有"myVar",除了_global。那個含有"myVar"物件(簡稱"o")的先被找到,則"o"物件內的"myVar"變數的值設定成5。

如果找不到"myVar"的參照,那麼Flash會建立一個新的參照"myVar"在範圍鏈最底層的物件裡。此物件只高於_global。

很顯然的,當我們設定變數的值,在範圍鏈內的物件的原型鏈未被檢查。所以儘管"myVar"參照存在於"o"的範圍鏈內,"o"內的"myVar"不會被設定新的值,除非範圍鏈內最後一個物件高於_global。

因此,要在_global物件內建立或設定屬性時,_global不能省略。

下面的程式碼說明了這種情況:

程式碼:
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
輸出:

The first 'a' reference found is:
undefined
undefined
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
輸出:

The first 'a' reference found is: 4
5
undefined
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
輸出:

The first 'a' reference found is: 4
4
undefined
5

 

With與範圍鏈

"with"讓我又愛又恨。因為它與範圍鏈互相影響,與巢狀函式有許多類似之處。舉例來說,我們在前面討論過的,我們可以使用"with"和巢狀函式在不詳細指定範圍的情況下設定變數。然而,它們是有一些差異的。最大的差異是連結到函式的建立。我在稍早提到過當函式建立時,目前的範圍鏈會依附到函式上。就像我們所看到的,這是因為activation物件會持續存在。如果在函式建立時,目前的範圍鏈被附加到函式上,那麼代表我們能夠用"with"把物件加到函式的範圍鏈中。但是實際上卻無法運作。看看下面的程式碼:

程式碼:
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
輸出:

5
undefined

在上面的程式碼中,我們使用"with"把物件放到範圍鏈的最上層。第4行顯示我們能夠取得"a"的值。根據目前的範圍鏈,我們建立一個新的函式(第5行)並將它指定為o2的一個新的方法。稍後,我們呼叫"o2.aMethod();",傳回undefined。表示範圍鏈內找不到"a",代表o1物件並未被加到函式的範圍鏈內。

當使用"with"將物件加到範圍鏈內時,函式建立時並不會將目前位置加入範圍鏈。

關於"this"參照呢?

至少在我的測試中,不管深度如何,"this"並不會被範圍鏈影響。"this"總是會參考呼叫函式的物件。

不同的是,函式的範圍鏈不會變化,代表函式中的"this"會隨著哪個呼叫此函式的物件而改變。我們可以回頭看看前面的例子:

程式碼:
1
                        2
                        3
                        4
                        5
                        6
                        7
                        8
                        9
                        10
                        11
                        12
                        13
                        14
                        15
                        
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
輸出:

6
7

"this"的意思是"呼叫函式的物件的參照"。我們稍早提到過,區域變數只是activation物件的屬性。如果使用var,我們附加函式到activation物件上,並在activation物件內執行這個函式,"this"參照會參考activation物件本身!依據這個情況,我們可能可以取得特定activation物件的參照,並將它存在其他地方(如果你可以找到這個寫法的用處)或在activation物件內使用陣列操作以取得或動態設定屬性(如果你可以找到這個寫法的用處)。

程式碼:
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();
輸出:

5

陣列操作與eval

actionscript裡面的eval是不被建議使用的嗎?eval和陣列操作有什麼不同?Ralf Bokelberg在這篇文章中提供了問題的解答,引述如下:

<quote>
你知道eval的真相嗎?

eval並不是不被建議使用的
eval並不是無用的
eval是很好用的

你如何存取目標物件

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

bokel
</quote>

是的,eval和陣列操作的最大差異在於eval能夠解決路徑的問題,而陣列操作只能取得物件的屬性。

根據我們之前討論過的,陣列操作與eval的另一個差異是它們的範圍鏈與原型鏈間的關係。使用eval或陣列操作取得單一變數(不是路徑)時,我們可以說陣列操作藉由搜尋物件的原型鏈取得變數;eval則是藉由搜尋目前的範圍鏈。再者,我們先前提到過,搜尋範圍鏈時也包含搜尋範圍鏈內每個物件的原型鏈。

 

你完全了解了嗎?

好啦,進入尾聲啦。根據我們先前討論過的,不使用Flash MX,你能夠知道下面這段程式碼輸出的結果嗎?

程式碼:
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();
  1. 4
  2. 5
  3. 6
  4. 7

最後更新日期:

10/02/2007 12:10:21 10/02/2007 12:05:58 05/31/2006 22:32:21

檔案大小

2402 Bytes 2402 Bytes 47881 Bytes

 





[本日志由 FindSome 于 2007-10-02 12:02 AM 编辑]
文章来自: 本站原创
引用通告: 查看所有引用 | 我要引用此文章
Tags:
相关日志:

评论: 0 | 引用: 0 | 查看次数: 3099
发表评论
昵 称:
密 码: 游客发言不需要密码.
内 容:
验证码: 验证码
选 项:
虽然发表评论不用注册,但是为了保护您的发言权,建议您注册帐号.
字数限制 1000 字 | UBB代码 关闭 | [img]标签 关闭