JavaScript中的範圍【轉自Realazy Blog】

來源:互聯網
上載者:User
原文: http://www.digital-web.com/articles/scope_in_javascript/

範圍(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所指。有四種情形值得特別注意:

呼叫對象的方法
在經典的物件導向編程中,我們需要識別和引用當前對象。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對象同名屬性的時候。

 

使用.apply()和.call()掌控上下文
在點擊按鈕的時候,我們真正需要的是能夠諮詢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特色,以解決這個問題。

.bind()之美
我並不是 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);
}

漂亮。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.