網頁制作poluoluo文章簡介:這篇文章將正面解決這個問題:簡述上下文(context)和作用域的定義,分析可以讓我們掌控上下文的兩種方法,最後深入一種高效的方案,它能有效解決我所碰到的90%的問題。
作用域(scope)是JavaScript語言的基石之一,在構建復雜程序時也可能是最令我頭痛的東西。記不清多少次在函數之間傳遞控制後忘記 this關鍵字引用的究竟是哪個對象,甚至,我經常以各種不同的混亂方式來曲線救國,試圖偽裝成正常的代碼,以我自己的理解方式來找到所需要訪問的變量。
這篇文章將正面解決這個問題:簡述上下文(context)和作用域的定義,分析可以讓我們掌控上下文的兩種方法,最後深入一種高效的方案,它能有效解決我所碰到的90%的問題。
JavaScript 程序的每一個字節都是在這個或那個運行上下文(execution context)中執行的。你可以把這些上下文想象為代碼的鄰居,它們可以給每一行代碼指明:從何處來,朋友和鄰居又是誰。沒錯,這是很重要的信息,因為 JavaScript社會有相當嚴格的規則,規定誰可以跟誰交往。運行上下文則是有大門把守的社區而非其內開放的小門。
我們通常可以把這些社會邊界稱為作用域,並且有充足的重要性在每一位鄰居的憲章裡立法,而這個憲章就是我們要說的上下文的作用域鏈(scope chain)。在特定的鄰裡關系內,代碼只能訪問它的作用域鏈內的變量。與超出它鄰裡的變量比起來,代碼更喜歡跟本地(local,即局部)的打交道。
具體地說,執行一個函數會創建一個不同的運行上下文,它會將局部作用域增加到它所定義的作用域鏈內。JavaScript通過作用域鏈的局部向全局攀升方式,在特定的上下文中解析標識符。這表示,本級變量會優先於作用域鏈內上一級擁有相同名字的變量。顯而易見,當我的好友們一起談論”Mike West”(本文原作者)時,他們說的就是我,而非bluegrass singer 或是Duke professor, 盡管(按理說)後兩者著名多了。
讓我們看些例子來探索這些含義:
<script type="text/javascript"> var ima_celebrity = "Everyone can see me! I'm famous!", the_president = "I'm the decider!"; function pleasantville() { var the_mayor = "I rule Pleasantville with an iron fist!", ima_celebrity = "All my neighbors know who I am!"; function lonely_house() { var agoraphobic = "I fear the day star!", a_cat = "Meow."; } } </script>
我們的全明星,ima_celebrity, 家喻戶曉(所有人都認識她)。她在政治上積極活躍,敢於在一個相當頻繁的基層上叫囂總統(即the_president)。她會為碰到的每一個人簽名和回答問題。就是說,她不會跟她的粉絲有私下的聯系。她相當清楚粉絲們的存在 並有他們自己某種程度上的個人生活,但也可以肯定的是,她並不知道粉絲們在干嘛,甚至連粉絲的名字都不知道。
而在歡樂市(pleasantville)內,市長(the_mayor)是眾所周知的。她經常在她的城鎮內散步,跟她的選民聊天、握手並親吻小孩。因為歡樂市(pleasantville)還算比較大且重要的鄰居,市長在她辦公室內放置一台紅色電話,它是一條可以直通總統的7×24熱線。她還可以看到市郊外山上的孤屋(lonely_house),但從不在意裡面住著的是誰。
而孤屋(lonely_house)是一個自我的世界。曠恐患者時常在裡面囔囔自語,玩紙牌和喂養一個小貓(a_cat)。他偶爾會給市長(the_mayor)打電話咨詢一些本地的噪音管制,甚至在本地新聞看到ima_celebrity後會寫些粉絲言語給她(當然,這是pleasantville內的ima_celebrity)。
每一個運行上下文除了建立一個作用域鏈外,還提供一個名為this的關鍵字。它的普遍用法是,this作為一個獨特的功能,為鄰裡們提供一個可訪問到它的途徑。但總是依賴於這個行為並不可靠:取決於我們如何進入一個特定鄰居的具體情況,this表示的完全可能是其他東西。事實上,我們如何進去鄰居家本身,通常恰恰就是this所指。有四種情形值得特別注意:
在經典的面向對象編程中,我們需要識別和引用當前對象。this極好地扮演了這個角色,為我們的對象提供了自我查找的能力,並指向它們本身的屬性。
<script type="text/javascript"> var deep_thought = { the_answer: 42, ask_question: function () { return this.the_answer; } }; var the_meaning = deep_thought.ask_question(); </script>
這個例子建立了一個名為deep_thought的對象,設置其屬性 the_answer為42,並創建了一個名為ask_question 的方法(method)。當deep_thought.ask_question()執行時, JavaScript為函數的呼叫建立了一個運行上下文,通過”.“運算符把this指向被引用的對象,在此是deep_thought這個對象。之後這個方法就可以通過this在鏡子中找到它自身的屬性,返回保存在 this.the_answer中的值:42。
類似地,當定義一個作為構造器的使用new關鍵字的函數時,this可以用來引用剛創建的對象。讓我們重寫一個能反映這個情形的例子:
<script type="text/javascript"> function BigComputer(answer) { this.the_answer = answer; this.ask_question = function () { return this.the_answer; } } var deep_thought = new BigComputer(42); var the_meaning = deep_thought.ask_question(); </script>
我們編寫一個函數來創建BigComputer對象,而不是直白地創建 deep_thought對象,並通過new關鍵字實例化deep_thought為一個實例變量。當new BigComputer()被執行,後台透明地創建了一個嶄新的對象。呼叫BigComputer後,它的this關鍵字被設置為指向新對象的引用。這個函數可以在this上設置屬性和方法,最終它會在BigComputer執行後透明地返回。
盡管如此,需要注意的是,那個deep_thought.the_question()依然可以像從前一樣執行。那這裡發生了什麼事?為何this在the_question內與BigComputer內會有所不同?簡單地說,我們是通過new進入BigComputer的,所以this表示“新(new)的對象”。在另一方面,我們通過 deep_thought進入the_question,所以當我們執行該方法時,this表示 “deep_thought所引用的對象”。this並不像其他的變量一樣從作用域鏈中讀取,而是在上下文的基礎上,在上下文中重置。
假如沒有任何相關對象的奇幻東西,我們只是呼叫一個普通的、常見的函數,在這種情形下this表示的又是什麼呢?
<script type="text/javascript"> function test_this() { return this; } var i_wonder_what_this_is = test_this(); </script>
在這樣的場合,我們並不通過new來提供上下文,也不會以某種對象形式在背後偷偷提供上下文。在此, this默認下盡可能引用最全局的東西:對於網頁來說,這就是 window對象。
比普通函數的呼叫更復雜的狀況,先假設我們使用函數去處理的是一個onclick事件。當事件觸發我們的函數運行,此處的this表示的是什麼呢?不湊巧,這個問題不會有簡單的答案。
如果我們寫的是行內(inline)事件處理函數,this引用的是全局window對象:
<script type="text/javascript"> function click_handler() { alert(this); // 彈出 window 對象 } </script> ... <button id='thebutton' onclick='click_handler()'>Click me!</button>
但是,如果我們通過JavaScript來添加事件處理函數,this引用的是生成該事件的DOM元素。(注意:此處的事件處理非常簡潔和易於閱讀,但其他的就別有洞天了。請使用真正的addEvent函數取而代之):
<script type="text/javascript"> function click_handler() { alert(this); // 彈出按鈕的DOM節點 } function addhandler() { document.getElementById('thebutton').onclick = click_handler; } window.onload = addhandler; </script> ... <button id='thebutton'>Click me!</button>
讓我們來短暫地運行一下這個最後的例子。我們需要詢問deep_thought一個問題,如果不是直接運行click_handler而是通過點擊按鈕的話,那會發生什麼事情?解決此問題的代碼貌似十分直接,我們可能會這樣做:
<script type="text/javascript"> function BigComputer(answer) { this.the_answer = answer; this.ask_question = function () { alert(this.the_answer); } } function addhandler() { var deep_thought = new BigComputer(42), the_button = document.getElementById('thebutton'); the_button.onclick = deep_thought.ask_question; } window.onload = addhandler; </script>
很完美吧?想象一下,我們點擊按鈕,deep_thought.ask_question被執行,我們也得到了“42”。但是為什麼浏覽器卻給我們一個undefined? 我們錯在何處?
其實問題顯而易見:我們給ask_question傳遞一個引用,它作為一個事件處理函數來執行,與作為對象方法來運行的上下文並不一樣。簡而言之,ask_question中的 this關鍵字指向了產生事件的DOM元素,而不是在BigComputer的對象中。DOM元素並不存在一個the_answer屬性,所以我們得到的是 undefined而不是”42″. setTimeout也有類似的行為,它在延遲函數執行的同時跑到了一個全局的上下文中去了。
這個問題會在程序的所有角落時不時突然冒出,如果不細致地追蹤程序的每一個角落的話,還是一個非常難以排錯的問題,尤其在你的對象有跟DOM元素或者window對象同名屬性的時候。
在點擊按鈕的時候,我們真正需要的是能夠咨詢deep_thought一個問題,更進一步說,我們真正需要的是,在應答事件和setTimeout的呼叫時,能夠在自身的本原上下文中呼叫對象的方法。有兩個鮮為人知的JavaScript方法,apply和call,在我們執行函數呼叫時,可以曲線救國幫我們達到目的,允許我們手工覆蓋this的默認值。我們先來看call:
<script type="text/javascript"> var first_object = { num: 42 }; var second_object = { num: 24 }; function multiply(mult) { return this.num * mult; } multiply.call(first_object, 5); // 返回 42 * 5 multiply.call(second_object, 5); // 返回 24 * 5 </script>
在這個例子中,我們首先定義了兩個對象,first_object和second_object,它們分別有自己的num屬性。然後定義了一個multiply函數,它只接受一個參數,並返回該參數與this所指對象的num屬性的乘積。如果我們呼叫函數自身,返回的答案極大可能是undefined,因為全局window對象並沒有一個num屬性除非有明確的指定。我們需要一些途徑來告訴multiply裡面的this關鍵字應該引用什麼。而multiply的call方法正是我們所需要的。
call的第一個參數定義了在業已執行的函數內this的所指對象。其余的參數則傳入業已執行的函數內,如同函數的自身呼叫一般。所以,當執行multiply.call(first_object, 5)時,multiply被呼叫,5傳入作為第一個參數,而this關鍵字被設置為first_object的引用。同樣,當執行multiply.call(second_object, 5)時,5傳入作為第一個參數,而this關鍵字被設置為second_object的引用。
apply以call一樣的方式工作,但可以讓你把參數包裹進一個數組再傳遞給呼叫函數,在程序性生成函數呼叫時尤為有用。使用apply重現上一段代碼,其實區別並不大:
<script type="text/javascript"> ... multiply.apply(first_object, [5]); // 返回 42 * 5 multiply.apply(second_object, [5]); // 返回 24 * 5 </script>
apply和call本身都非常有用,並值得貯藏於你的工具箱內,但對於事件處理函數所改變的上下文問題,也只是送佛到西天的中途而已,剩下的還是得我們來解決。在搭建處理函數時,我們自然而然地認為,只需簡單地通過使用call來改變this的含義即可:
function addhandler() { var deep_thought = new BigComputer(42), the_button = document.getElementById('thebutton'); the_button.onclick = deep_thought.ask_question.call(deep_thought); }
代碼之所以有問題的理由很簡單:call立即執行了函數(譯注:其實可以用一個匿名函數封裝,例如the_button.onclick = function(){deep_thought.ask_question.call(deep_thought);},但比起即將討論的bind來,依然不夠優雅)。我們給onclcik處理函數一個函數執行後的結果而非函數的引用。所以我們需要利用另一個JavaScript特色,以解決這個問題。
我並不是 Prototype JavaScript framework的忠實粉絲,但我對它的總體代碼質量印象深刻。具體而言,它為Function對象增加一個簡潔的補充,對我管理函數呼叫執行後的上下文產生了極大的正面影響:bind跟call一樣執行相同的常見任務,改變函數執行的上下文。不同之處在於bind返回的是函數引用可以備用,而不是call的立即執行而產生的最終結果。
如果需要簡化一下bind函數以抓住概念的重點,我們可以先把它插進前面討論的乘積例子中去,看它究竟是如何工作的。這是一個相當優雅的解決方案:
<script type="text/javascript"> var first_object = { num: 42 }; var second_object = { num: 24 }; function multiply(mult) { return this.num * mult; } Function.prototype.bind = function(obj) { var method = this, temp = function() { return method.apply(obj, arguments); }; return temp; } var first_multiply = multiply.bind(first_object); first_multiply(5); // 返回 42 * 5 var second_multiply = multiply.bind(second_object); second_multiply(5); // 返回 24 * 5 </script>
首先,我們定義了first_object, second_object和multiply函數,一如既往。細心處理這些後,我們繼續為Function對象的prototype定義一個bind方法,這樣的話,我們程序裡的函數都有一個bind方法可用。當執行multiply.bind(first_object)時,JavaScript為bind方法創建一個運行上下文,把this置為multiply函數的引用,並把第一個參數obj置為first_object的引用。目前為止,一切皆順。
這個解決方案的真正天才之處在於method的創建,置為this的引用所指(即multiply函數自身)。當下一行的匿名函數被創建,method通過它的作用域鏈訪問,obj亦然(不要在此使用this, 因為新創建的函數執行後,this會被新的、局部的上下文覆蓋)。這個this的別名讓apply執行multiply函數成為可能,而傳遞obj則確保上下文的正確。用計算機科學的話說,temp是一個閉包(closure),它可以保證,需要在first_object的上下文中執行multiply,bind呼叫的最終返回可以用在任何的上下文中。
這才是前面說到的事件處理函數和setTimeout情形所真正需要的。以下代碼完全解決了這些問題,綁定deep_thought.ask_question方法到deep_thought的上下文中,因此能在任何事件觸發時都能正確運行:
function addhandler() { var deep_thought = new BigComputer(42), the_button = document.getElementById('thebutton'); the_button.onclick = deep_thought.ask_question.bind(deep_thought); }
漂亮。