js教程:javascript作用域(Scope)

作用域(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' οnclick='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);
}
漂亮。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章