讀古今文學網 > 編寫高質量代碼:改善JavaScript程序的188個建議 > 建議85:謹慎處理對象的Scope >

建議85:謹慎處理對象的Scope

在面向對象的JavaScript編程中,包裝繼承了大量的對象,同時對像之間還有很多複雜的引用關係,這就使得很多在function被調用時的Scope不是想要的Scope。產生這些問題的原因都源於沒有深入瞭解JavaScript的機制。

在JavaScript中,function是作用於詞法範圍而不是動態運行範圍的,也就是說,function的作用範圍是它聲明的範圍,而不是它在執行時的範圍。簡單地說,一個function在執行時的上下文環境Context在其定義時就固定下來了,就是它定義時的作用範圍。有一點需要注意,很多時候動態地將某個方法注入到一個對像內部,然而在運行時總是得不到想要的上下文環境,這是因為沒有正確理解JavaScript的Scope。

當一個function被JavaScript引擎調用執行時,這個function的Scope才起作用,並且被加到Scope鏈中。然後,將一個名為Call Object的調用對像或運行對像加到Scope的最前面。這個調用對像在初始化時會加入一個arguments屬性,用來引用function調用時的參數。如果這個function顯式地定義了調用參數,那麼這些參數也會被加入到這個對象中,之後在這個函數運行過程中所有的局部變量也都將包含在這個對象中。這也就是在function體內部既可以通過arguments數組,又可以直接通過顯式定義參數名來引用調用時傳入參數的原因。

調用對像和通過this關鍵字引用的對象是兩個概念,調用對象是function在運行時的Scope,其中包含了function在運行時的全部參數和局部變量。通過this關鍵字引用對象,是當function作為一個對象的方法運行時對這個對象進行引用。如果這個function沒有被定義在一個對像中,傳給this的對象是全局對象,那麼在這個function內部通過this取到的變量就是全局定義的變量。例如,下面代碼運行結果應該彈出「Hello,I am Qin Jian」。


var hello=\"Hello,I am Shao Yu!\";

function sayHello{

var hello=\"Hello,I am Qin Jian.\"

function anotherFun{

alert(hello);

}

anotherFun;

}

sayHello;


在JavaScript中,允許定義匿名function和在function中嵌套定義另一個function。由於Scope鏈的作用,function的內部總是可以訪問外部的變量而外部卻不能訪問內部的變量。另外,由於Call Object的機制,可以使用function嵌套定義和調用來做很多事情。在JavaScript中,這樣的調用稱為Closure閉包。例如,下面代碼使用閉包生成唯一ID。


uniqueID=(function{

var id=0;

return function{

return id++;

};

});

alert(uniqueID);//0

alert(uniqueID);//1

alert(uniqueID);//2


上面這段代碼很清楚地說明了閉包做了什麼事情。當外層的function被執行時,它的Scope被加入到Scope chain上,然後為它創建Call Object並加入到Scope中,之後又創建了局部變量id並將它保存在該function的Call Object中。如果沒有「return function{return id++;};」這條語句,那麼外層的function將會運行結束並退出,同時它的Call Object會被釋放,Scope會從Scope chain上移除。由於「return function{return id++;};」這條語句創建了一個內部的function,並且將其引用返回給一個變量,因此內部function的Scope會被添加到之前外部function的Scope之下,使得在外部function運行結束後它的Scope不能被撤銷和釋放。這樣就是用外部function的Call Object保存了變量id,並且除了內部的function以外沒有別的程序能訪問到這個變量。雖然看起來有些複雜,但是閉包確實是一項非常有用的功能,經常用來保存變量、控制訪問域等。例如,在下面示例中利用Call Object和閉包保存數據。


function makefunc(x){

return function{

return x;

}

}

var a=[makefunc(\"I am Qin Jian\"),makefunc(\"I am shao yu\"),makefunc(\"I am xu ming\")];

alert(a[0]);//I am Qin Jian

alert(a[1]);//I am shao yu

alert(a[2]);//I am xu ming


JavaScript中提供了兩個非常有意思的方法:call和apply,使用它們可以將一個function作為另一個Object的對象方法來調用,也就是說,可以在選擇function調用時,將其傳入給this關鍵字的對象。這兩個方法第一個參數是相同的,都是想要傳入給this關鍵字的對象。不同之處是,call方法直接將function參數列在後面,而apply方法是將所有function參數以一個數組的形式傳入。例如:


var fun=function(arg1,arg2){

//...

}

fun.call(object,arg1,arg2);

fun.apply(object,[arg1,arg2]);


這兩個方法在面向對象的JavaScript編程中是非常有用的,因為有時希望給某個對象添加一個事件監聽,然而回調方法的context卻不一定是需要的,這時就需要使用call或apply方法了。

eval方法用於執行某個字符串中的JavaScript腳本,並返回執行結果。它允許動態生成的變量、表達式、函數調用、語句段得到執行,使得JavaScript程序的設計過程更為靈活,如通過Ajax方式從服務器端獲得代表JavaScript腳本的字符串,然後就可以用eval方法在瀏覽器端執行這段腳本。傳統的Ajax通信設計更多的是在服務器端與瀏覽器端交換數據,但是在eval方法的支持下可以在兩者之間交換邏輯,這是一種很有趣的事情。

Eval方法很靈活也比較簡單,在調用此方法時將要執行的JavaScript腳本字符串作為參數。比較常用的就是將服務器端發送過來的JSON字符串在瀏覽器端轉化為JSON對象。例如,使用eval方法將JSON字符串轉換為對象。


var JSONString=\"{\'name\':{\'qinjian\':\'I am qinjian\',\'shaoyu\':\'I am shaoyu\'}}\";

var JSONObject=eval(\"(\"+JSONString+\")\");

alert(JSONObject.name.qinjian)


在上面的代碼中,在JSON字符串中又加上了一對括號,這樣做可以迫使eval方法在評估JavaScript代碼時強制將原最外層括號內的內容作為表達式來執行從而得到JSON對象,而不是作為語句來執行。因此,只有用新的小括號將原來的JSON字符串包含起來才能夠轉換出所需的JSON對象。

對於執行一般的包含在字符串中的JavaScript語句,自然就不需要像上面那樣再次添加括號了,例如:


function testStatement{

eval(\"var m=1\");

alert(m);

}

testStatement;


上面的函數會在彈出的提示對話框中輸出變量m的值。雖然eval函數中的語句只是一個簡單的賦值,但是有一個問題值得注意,那就是eval中的語句是在什麼樣的Scope中執行,為了更好地說明這個問題,執行下面的代碼:


function testStatement{

eval(\"var m=1\");

alert(m);

}

testStatement;

alert(typeof m);


可以得到,變量m所在的Scope是在testStatement函數內,從eval調用的位置來看,這個執行結果是合理的。接下來用window.eval替換上面代碼中的eval後,在Firefox瀏覽器(注意是Firefox)上執行代碼,從執行後得到的結果中可以發現,這時的m的作用域變成了window,也就是說,m變成了一個全局變量。那麼在IE上執行會如何呢?經測試發現,使用window.eval和eval會在IE上得到相同的結果,即m的作用域在testStatement函數中。但IE提供了另外一個方法execScript,它會將輸入的JavaScript腳本字符串放到全局作用域下執行。總結一下:eval方法是將輸入的代碼在局部作用域下執行,若要使JavaScript字符串中包含的代碼具有全局作用域執行效果,要麼把eval放到全局作用域下調用,要麼在Firefox中使用window.eval,在IE中使用execScript。最後需要提醒的是,因為eval的執行效率較低,所以在程序中最好不要頻繁使用。為了避免Scope問題,應該注意下列幾方面內容。

(1)巧用閉包

瞭解了閉包利用Call Object的產生原理後,就可以很容易利用閉包,如利用namespace隔離和保存局部數據等。同時,閉包也很容易使this實例不是我們想要的this實例,這時就可以利用內層能夠訪問外層變量的特點將外層this實例賦給一個變量,內層可以通過這個變量順利訪問外層this實例。


switchAds:function(index){

var_this=this;

dojo.fadeOut({

node:_this.adBack,

duration:500,

onEnd:function{

dojo.fadeIn({

node:_this.adBack,

duration:500,

onEnd:function{

_this.currentAd=index;

}

}).play;

}

}).play;

}


(2)使用call和apply指定function調用時的Scope

我們經常會提供一種類似回調函數的機制,在設計某個接口的時候並不提供函數的實現而只負責調用該接口。例如,對外提供一個onclick事件接口,由於JavaScript的Scope機制屬於詞法範圍而不是動態運行範圍,因此回調函數運行的Scope往往不是我們想要的。對於這種情況,可以在註冊回調函數時將Scope一起傳進來,在需要調用該回調函數時使用call和apply方法來調用這個函數,同時將需要的Scope傳遞給這個函數。例如,dojo的事件機制就是這樣處理的,在註冊回調函數的同時可以指定函數調用的Scope。


dojo.connect(node,\"onclick\",this,this._collapse);


(3)慎用eval

eval可以動態地從字符串中執行代碼,它使JavaScript的功能更加強大。通常,eval有全局或局部兩種運行Scope方式。當其運行在一個局部的function中時,需要注意,這時eval運行在這個function的Call Object中,它可以通過this關鍵字訪問到這個function的Scope。另外,eval方法運行效率非常低,並且運行的腳本是未經驗證的,因此在使用eval方法時要十分慎重。