今天我想簡單討論下關於Javascript的範圍和this變數。“範圍”的概念就是說,我們的代碼能夠從哪裡去訪問某些函數或者變數,也就是它們所存在的上下文,或者說就是它們被執行的地方。
你可能已經見過有的人寫類似這樣的代碼:
function someFunc() {var _this = this;something.on(click, function() {console.log(_this);});};
可是你卻搞不懂其中“var _this=this;”這一句到底是要幹嘛。希望本篇文章能澄清這種疑惑。
第一種範圍是全域範圍(Global Scope),它的定義很簡單。如果一個函數或者變數是全域的,那麼在任何地方都能夠訪問它們。在瀏覽器裡,全域範圍其實就是window對象。所以如果你在代碼裡這樣定義一個變數:
var x = 9;
那麼實際上你是在window對象上建立一個叫x的屬性,並給它賦值為9。當然如果你願意也可以這樣寫:
window.x = 9;
不過既然它是全域變數,一般人都不會這樣做。window對象是全域的,它上面的所有屬性都可以在代碼裡的任何地方直接存取到。
另外一種,也是最後一種範圍,就是本地範圍(Local Scope)。JavaScript在函數水平上建立本地範圍,比如:
function myFunc() {var x = 5;};console.log(x); //undefined
因為x是在myFunc()之內聲明的,所以它僅僅在myFunc()內是可以訪問到的。
一點小提醒:
如果你沒有使用var關鍵字來聲明一個變數,那麼它會自動變成全域的。所以這樣寫也是可行的:
function myFunc() {x = 5;});console.log(x); //5
但這並不是一個好主意。這樣做會把全域範圍搞亂,所以這種做法很不推薦。你應該盡量少地在全域範圍裡聲明變數。之所以像jQuery一類的函數庫會這樣做,也是出於這個原因:
(function() {var jQuery = { /* all my methods go here */ };window.jQuery = jQuery.})();
先把所有的東西都放在一個匿名函數的函數體裡,然後再立刻執行這個函數,這樣一來,在這個函數裡聲明的所有變數就都只存在於本地範圍了。而在最後,你再把jQuery對象綁定到window對象上,這樣jQuery就是全域的了,而你也就進而把所有的東西變成是全域可訪問到的了(譯者註:這些變數函數什麼的仍然存在於本地範圍,所以不能從外面直接存取,當然,要訪問的話,只能通過這個唯一的全域的介面:jQuery,這就是所謂的閉包的最大功德)。儘管在這段範例程式碼裡我把jQuery簡化到了不能再簡化的地步,可是本質上說,它的源碼的工作原理就是這樣的。如果你想知道更多細節,強烈推薦你讀一下Paul Irish的文章:10 Things I learned from the jQuery Source。
因為本地範圍以函數為單位,所以在一個函數內定義的函數,是可以訪問外面這個包含它的函數的本地變數的:
function outer() {var x = 5;function inner() {console.log(x); //5 }inner();}
不過反過來卻不行,outer()函數並不能訪問inner()裡面的任何變數:
function outer() {var x = 5;function inner() {console.log(x); //5 var y = 10;}inner();console.log(y); //undefined}
目前來看,這都是些很簡單很基本的東西。不過,如果我們要來審視一番this關鍵字,情況就變複雜很多了,我想咱們都遇見過這種情況(譯者註:關於這裡有爭議,見後面註解【1】。):
$(myLink).on(click, function() {console.log(this); //points to myLink (as expected)$.ajax({//ajax set upsuccess: function() {console.log(this); //points to the global object. Huh?}});});
每次在你的函數被執行的時候,this變數都是被自動賦值的,至於它的具體值到底是什麼,這取決於該函數被呼叫的方式。JavaScript裡面有幾種主要的呼叫函數的方式,我並不打算現在在這裡一一敘述,不過常用的就那麼三種:作為一個對象的方法被呼叫;或者作為函數獨自被呼叫;或者作為一個事件的處理器(event handler)被呼叫。不同的來電者式將導致this的值是不同的:
function foo() {console.log(this); //global object 譯者註:其實就是window};myapp = {};myapp.foo = function() {console.log(this); //points to myapp object}var link = document.getElementById(myId);link.addEventListener(click, function() {console.log(this); //points to link}, false);
情況一目瞭然。MDN對於第三種情況有詳細的解釋:
通常來說,在一個事件處理器被執行過程中,我們都希望能追蹤到觸發這個事件的對象,尤其是有時候可能會在若干個相似的對象上綁定同一個事件處理器(譯者註:比如說你在一系列連結化物件上綁定了同一個處理click事件的處理器)。當我們用addEventListener()來綁定一個函數的時候,this的值會被改變,注意:this的值實際上是由呼叫者傳遞給函數的。
所以,現在,再回過頭來看一開始的關於“var _this = this;”這一句的用意的疑問,我們才猛然發現已經離答案不遠了。
$(#myLink).on(click, function() {})這句的意圖是當這個DOM元素被點擊時,這個函數便被執行。可是由於這個函數是作為一個事件處理器被呼叫的,所以this變數會指向ID為myLink的DOM元素。而你在Ajax請求裡指定的success方法只是一個常規的函數,所以當它被執行的時候,this被賦值為全域對象。(譯者註:關於這裡有爭議,見後面註解【1】。)
上述原因就是為什麼你總會見到有人寫:var _this = this或者var that = this,或者類似的東西,這樣做的目的是把當前this的值備份留作以後不時之需。關於下面這段代碼裡第二個console.log()究竟應該輸出什麼值,很多人提出了異議,這個問題我以後再討論(譯者註:看來,作者本人沒迴避這個問題,我也是在後面提出異議者之一)。
$(myLink).on(click, function() {console.log(this); //points to myLink (as expected)var _this = this; //store reference$.ajax({//ajax set upsuccess: function() {console.log(this); //points to the global object. Huh?console.log(_this); //better!}});});
另外也有一些呼叫函數的方法是可以主動明確地指定this的值的,不過因為這篇文章現在已經夠長了,所以不如等改天再詳細討論吧。如有問題請留言,我會一一回複。
註解【1】:
這個問題已經有人在原文下面的評論中提到,就是
success: function() {
console.log(this); //points to the global object. Huh?
}
輸出的並不是window,而是另外一個對象,看起來是jQuery用來記錄Ajax設定參數的一個對象。
假設說我在一個載入了jQuery的網頁裡插入了一個id為vince的標籤,然後我執行:
$(#vince).on(click, function() { console.log(this);// options :var my_city=Washington,USA;var my_key=xxxxxxxxxxxxxxxxxxxxxx;var no_of_days=2;// build URI:var uri=http://free.worldweatheronline.com/feed/weather.ashx?q=+my_city+&key=+my_key+&format=json&no_of_days=+no_of_days+&includeLocation=yes;// uri-encode it to prevent errors :uri=encodeURI(uri); $.ajax({ type: POST,url: uri,complete: function() { console.log(this); } });});
我本想類比用Ajax訪問一個公用的並且仍然活躍的web service,用來做這個示範,而不是用我本地的server,但是找了半天也找不到,最後找到一個提供天氣情況的,可是需要註冊才有API碼,我沒有時間去完成註冊,所以上面的my_key是“xxxxx”,因此,這個Ajax請求將不會成功返回。所以我沒有像原文那樣使用success事件處理函數,而是使用了complete事件,這樣不論如何保證它都會被調用。那麼看到的結果其實是:
實際上關於這個被輸出的東西到底是怎麼來的,我們可以在Chrome裡面用單步調試的方法來追蹤,不過前提是,要載入非min版本的jQuery源碼,不然沒有人看的懂,所以這需要時間,我想還是等我比較閑的時候再來搞吧。