語言特性 引用 JavaScript的一個重要的方麵是引用的概念。引用就是指向對象實際位置的指針。這是一項極其強大的功能。前提是,實際的對象決不是一個引用:字符串總是一個字符串,數組總是一個數組。然而,多個變量可以引用相同的對象。JavaScript就是以這種引用引用機製為基礎。通過維護一係列的指向其它對象的引用,語言為你提供了更大的彈性。
另外,對象能包括一係列的屬性,這些屬性簡單地引用其它對象(如字符串,數字,數組等等)。當幾個變量指向相同對象時,修改底層對象類型將會在所有的指點向它的變量上有所反映。例2-1即此一例,兩個變量指向同一個對象,但是對對象內容的修改的反映是全局的。
程序2-1. 多變量引用單個對象的示例
//設置obj為一個空對象
var obj = new Object();
//objRef現在引用了別的對象
var objRef = obj;
//修改原始對象的屬性
obj.oneProperty = true;
//我們可以發現該變化在兩個變量中都可以看到
//(因為他們引用了同一個對象)
alert( obj.oneProperty === objRef.oneProperty );
我從前提到過自更改的對象在JavaScript裏非常少見的。讓我們看一個發生這一狀況的實例。數組對象能夠用push方法給它自己增加額外的項。因為在數組對象的核心,值是作為對象的屬性存儲的,結果類似程序2-1中的情形,一個對象成為全局被改動的(導致了多個變量的值被同時改變)。見程序2-2.
程序2-2. 自修改對象的例子
//創建一組項目的數組
var items = new Array( "one", "two", "three" );
//創建一個對項目數組的引用
var itemsRef = items;
//給原始數組添加一項
items.push( "four" );
//兩個數組的長度應該相同,
//因為它們都指向相同的數組對象
alert( items.length == itemsRef.length );
記住這一點是很重要的:引用總是隻指向最終被引用的對象,而不會是引用本身。例如,在Perl語言裏,很可能有一個引用指向另一個也是引用的變量。但在JavaScript裏,它會沿著引用鏈向下追溯直到指向核心的對象。程序2-3演示了這種情形,物理的目標已經改變而引用仍然指向原來的對象。
程序2-3. (見#9 oerrite 的回複)
// 設置items為一個字符串的數組(對象)
var items = new Array( "one", "two", "three" );
// 設置itemsRef為對items的引用
var itemsRef = items;
//讓items指向一個新的對象
items = new Array( "new", "array" );
// items和itemsRef現在指向不同的對象
// items指向new Array( "new", "array" )
// itemsRef則指向new Array( "one", "two", "three" )
alert( items !== itemsRef );
最後,讓我們來看一個陌生的例子,表麵似乎是一個自修改的對象,卻作用於一個新的未被引用的對象。當執行字符串串聯時,結果總是一個新的字符串對象,而非原字符串更改後的版本。這在程序2-4中可以看出。
程序2-4. 對象修改作用於一個新的對象而非自修改對象的示例
//讓item等於一個新的字符串對象
var item = "test";
//itemRef也引用相同的字符串對象
var itemRef = item;
//在字符串對象上串聯一個新的對象
//注意:這創建了一個新的對象,並不修改初始對象
item += "ing";
//item和itemRef的值並不相等,因為
//一個全新的對象被創建了
alert( item != itemRef );
如果你剛剛接觸,引用可能是個令人頭大的刁鑽話題。然而,理解引用是如何工作的對於編寫良好、幹淨的JavaScript代碼是極其重要的。接下來的幾節我們將探究幾種未必新鮮和令人激動的,但是同樣對編寫良好、幹淨的代碼很重要的特性。
函數重載和類型檢查 其它麵向對象的語言(比如Java)的一種共有的特性是“重載”函數的能力:傳給它們不同數目或類型的參數,函數將執行不同操作。雖然這種能力在JavaScript中不是直接可用的,一些工具的提供使得這種探求完全成為可能。
在JavaScript的每一個函數裏存在一個上下文相關的名為arguments的變量,它的行為類似於一個偽數組,包含了傳給函數的所有參數。參數不是一真正的數組(意味著你不能修改它,或者調用push()方法增加新的項),但是你可以以數組的形式訪問它,而且它也的確有一個length屬性。程序2-5中有兩個示例。
程序2-5. JavaScript中函數重載的兩個示例
//一個簡單的用來發送消息的函數
function sendMessage( msg, obj ) {
//如果同時提供了一個消息和一個對象
if ( arguments.length == 2 )
//就將消息發給該對象
obj.handleMsg( msg );
//否則,剛假定隻有消息被提供
else
//於是顯示該消息
alert( msg );
}
//調用函數,帶一個參數 – 用警告框顯示消息
sendMessage( "Hello, World!" );
//或者,我們也可以傳入我們自己的對象用
//一種不同方式來顯示信息
sendMessage( "How are you?", {
handleMsg: function( msg ) {
alert( "This is a custom message: " + msg );
}
});
//一個使用任意數目參數創建一個數組的函數
function makeArray() {
//臨時數組
var arr = [];
//遍曆提交的每一個參數
for ( var i = 0; i < arguments.length; i++ ) {
arr.push( arguments[i] );
}
//返回結果數組
return arr;
}
另外,存在另一種斷定傳遞給一個函數的參數數目的方法。這種特殊的方法多用了一點點技巧:我們利用了傳遞過來的任何參數值不可能為undefined這一事實。程序2-6展示一了個簡單的函數用來顯示一條錯誤消息,如果沒有傳給它,則提供一條缺省消息。
程序2-6: 顯示錯誤消息和缺省消息
function displayError( msg ) {
//檢查確保msg不是undefined
if ( typeof msg == 'undefined' ) {
//如果是,則設置缺省消息
msg = "An error occurred.";
}
//顯示消息
alert( msg );
}
typeof語句的使用引入了類型檢查。因為JavaScript(目前)是一種動態類型語言,使得這個話題格外有用而重要的話題。有許多種方法檢查變量的類型;我們將探究兩種特別有用的。
第一種檢查對象類型的方式是使用顯式的typeof操作符。這種有用的方法給我們一個字符串名稱,代表變量內容的類型。這將是一種完美的方案,除非變量的類型或者數組或自定義的對象如user(這時它總返回"ojbect",導致各種對象難以區分)。
這種方法的示例見程序2-7
程序2-7. 使用typeof決定對象類型的示例
//檢查我們的數字是否其實是一個字符串
if ( typeof num == "string" )
//如果是,則將它解析成數字
num = parseInt( num );
//檢查我們的數組是否其實是一個字符串
if ( typeof arr == "string" )
//如果是,則用逗號分割該字符串,構造出一個數組
arr = arr.split(",");
檢查對象類型的第二種方式是參考所有JavaScript對象所共有的一個稱為constructor的屬性。該屬性是對一個最初用來構造此對象的函數的引用。該方法的示例見程序2-8。
程序2-8. 使用constructor屬性決定對象類型的示例
//檢查我們的數字是否其實是一個字符串
if ( num.constructor == String )
//如果是,則將它解析成數字
num = parseInt( num );
//檢查我們的字符串是否其實是一個數組
if ( str.constructor == Array )
//如果是,則用逗號連接該數組,得到一個字符串
str = str.join(',');
表2-1顯示了對不同類型對象分別使用我所介紹的兩種方法進行類型檢查的結果。表格的第一列顯示了我們試圖找到其類型的對象。每二列是運行typeof Variable(Variable為第一列所示的值)。此列中的所有結果都是字符串。最後,第三列顯示了對第一列包含的對象運行Variable.constructor所得的結果。些列中的所有結果都是對象。
表2-1. 變量類型檢查
———————————————————————————————
Variable typeof Variable Variable.constructor
———————————————————————————————
{an:"object"} object Object
["an","array"] object Array
function(){} function Function
"a string" string String
55 number Number
true boolean Boolean
new User() object User
——————————————————————————————————
使用表2-1的信息你現在可以創建一個通用的函數用來在函數內進行類型檢查。可能到現在已經明顯,使用一個變量的constructor作為對象類型的引用可能是最簡單的類型檢查方式。當你想要確定精確吻合的參數數目的類型傳進了你的函數時,嚴格的類型檢查在這種可能會大有幫助。在程序2-9中我們可以看到實際中的一例。
程序2-9. 一個可用來嚴格維護全部傳入函數的參數的函數
//依據參數列表來嚴格地檢查一個變量列表的類型
function strict( types, args ) {
//確保參數的數目和類型核匹配
if ( types.length != args.length ) {
//如果長度不匹配,則拋出異常
throw "Invalid number of arguments. Expected " + types.length +
", received " + args.length + " instead.";
}
//遍曆每一個參數,檢查基類型
for ( var i = 0; i < args.length; i++ ) {
//如JavaScript某一項類型不匹配,則拋出異常
if ( args[i].constructor != types[i] ) {
throw "Invalid argument type. Expected " +
types[i].name +", received " +
args[i].constructor.name + " instead.";
}
}
}
//用來打印出用戶列表的一個簡單函數
function userList( prefix, num, users ) {
//確保prefix是一個字符串,num是一個數字,
//且user是一個數組
strict( [ String, Number, Array ], arguments );
//循環處理num個用戶
for ( var i = 0; i < num; i++ ) {
//顯示一個用戶的信息
print( prefix + ": " + users[i] );
變量類型檢查和參數長度校驗本身是很簡單的概念,但是可用來實現複雜的方法,給開發者和你的代碼的使用者提供更好的體驗。接下來,我們將探討JavaScript中的作用域以及怎麼更好的控製它。
作用域 作用域是JavaScript中一個較難處理的特性。所有麵向對象的編程語言都有某種形式的作用域;這要看是什麼上下文約束著作用域。在JavaScript裏,作用域由函數約束,而不由塊約束(如while,if,和for裏的語句體)。最終可能使得一些代碼的運行結果表麵上顯得怪異(如果你來自一種塊作用域語言的話)。程序2-10的例子說明了“函數作用域代碼”的含義。
代碼2-10. JavaScript中變量作用域是怎樣工作的例子
//設置一個等於"test"的全局變量foo
var foo = "test";
//在if塊中
if ( true ) {
//設置foo為"new test"
//注意:這仍然是在全局作用域中
var foo = "new test";
}
//正如我們在此處可見,foo現在等於"new test"
alert( foo == "new test" );
//創建一個修改變量foo的函數
function test() {
var foo = "old test";
}
//調用時,foo卻駐留在是在函數的作用域裏麵
test();
//確認一下,foo的值仍然是"new test"
alert( foo == "new test" );
在程序2-10中你會發現,變量位於在全局作用域。基於瀏覽器的JavaScript有趣的一麵是,所有的全局變量實際上都是window對象的屬性。盡管一些老版本的Opera瀏覽器或Safari瀏覽器不是這樣,假定瀏覽器這樣工作通常是一個很好的經驗規則。程序2-11展示了一個這種例子。
程序2-11. JavaScript的全局變量與window對象的例子
//全局變量,包含字符串"test"
var test = "test";
//你會發現,我們的全局變量和window的test屬性是相同的
alert( window.test == test );
最後,讓我們來看看當一個變量漏定義時會怎樣。程序2-12裏,變量foo在test()的作用域裏被賦值。但是,程序2-12裏實際並沒有(用var foo)定義變量的作用域。當變量foo沒有明確定義時,它將成為全局變量,即使它隻在函數的上下文使用。
程序2-12. 隱式全局變量聲明的示例
//一個為變量foo賦值的函數
function test() {
foo = "test";
}
//調用函數為foo賦值
test();
//我們發現foo現在是全局變量了
alert( window.foo == "test" );
到目前應該很明顯,盡管JavaScript的作用域不如塊作用域語言的嚴格,它還是相當強大和有特色的。尤其是與下節中敘述的閉包的概念結合起來時,JavaScript語言的強大將展露無遺。
閉包 閉包意味著內層的函數可以引用存在於包繞它的函數的變量,即使外層的函數的執行已經終止。這一特殊的論題可能是非常強大又非常複雜的。我強烈推薦你們參考本節後麵將提及的站點,因為它有一些關於閉包這一話題的精彩的信息。
我們先來看程序2-13所示的閉包的兩個簡單例子。
程序2-13. 閉包改善的代碼清晰性的兩例
//得到id為"main"的元素
var obj = document.getElementById("main");
//改變它的邊框樣式
obj.style.border = "1px solid red";
//初始化一個1秒鍾以後被調用的回調函數
setTimeout(function(){
//此函數將隱藏該元素
obj.style.display = 'none';
}, 1000);
//用來延遲顯示消息的通用函數
function delayedAlert( msg, time ) {
//初始化一個被封套的函數
setTimeout(function(){
//此函數使用了來自封套它的函數的變量msg
alert( msg );
}, time );
}
//調用函數delayedAlert,帶兩個參數
delayedAlert( "Welcome!", 2000 );
第一個對setTimeout的函數調用,展示了一個的JavaScript新手遇到問題的通俗的例子。在JavaScript新手的程序裏像這樣的代碼時常可以看到:
setTimeout("otherFunction()", 1000);
//或者甚至
setTimeout("otherFunction(" + num + "," + num2 + ")", 1000);
使用閉包的概念,完全可能的把這種混亂的代碼清理掉。第一個例子很簡單;有一個回調函數在調用setTimeout函數以後1000微秒以後被調用,而它仍引用了變量obj(定義在全局範圍,指向id為"main"的元素)。定義的第二個函數,delayedAlert,展示了一種解決出現的setTimeout混亂的方案,以及函數作用域內可以有閉包的能力。
你們應該可以發現,當在代碼中使用這種簡單的閉包時,你所寫的東西的清晰性將會提高,免於陷入語法的迷霧之中。
我們來看一個閉包可能帶來的有有趣的副作用。在某些函數化的編程語言裏,有一個叫做
currying的概念。本質上講,currying是就是為函數的一些參數預填入值,創建一個更簡單的新函數的方法。代碼2-14裏有一個簡單的currying的例子,創建了向另一個函數預填一個參數而得的新函數。
代碼2-14. 使用閉包的函數currying
//生成做加法的新函數的函數
function addGenerator( num ) {
//返回一個簡單函數用來計算兩個數的加法,
//其中第一個數字從生成器中借用
return function( toAdd ) {
return num + toAdd
};
}
//addFive現在是接受一個參數的函數,
//此函數將給參數加5,返回結果數字
var addFive = addGenerator( 5 );
//這裏我們可以看到,當傳給它參數4的時候
//函數addFive的結果為9
alert( addFive( 4 ) == 9 );
閉包還能解決另一個常見的JavaScript編碼方麵的問題。JavaScript新手趨向於在全局作用域裏放置許多變量。這一般被認為是不好的習慣,因為那些變量可能悄悄地影響其它的庫,導致令人迷惑的問題的產生。使用一個自執行的、匿名的函數,你可以從根本上隱藏所有的通常的全局變量,使它們對其它代碼不可見,如程序2-15所示。
代碼2-15. 使用匿名函數從全局作用域隱藏變量的例子
//創建一個用作包裝的匿名函數
(function(){
//這個變量通常情況下應該是全局的
var msg = "Thanks for visiting!";
//為全局對象綁定新的函數
window.onunload = function(){
//使用了“隱藏”的變量
alert( msg );
};
//關閉匿名函數並執行之
})();
最後,讓我們來看使用閉包時出現的一個問題。閉包允許你引用存在於父級函數中的變量。然而,它並不是提供該變量創建時的值;它提供的是父級函數中該變量最後的值。你會看到這個問題最通常是在一個for循環中。有一個變量被用作迭代器(比如i),在for內部新的函數被創建,並使用了閉包來引用該迭代器。問題是,當新的閉包函數被調用時,它們將會引用該iterator最後的值(比如,一個數組的最後位置),而不是你所期望的那個。程序2-16的例子說明,使用匿名函數激發作用域,在其中創建一個合乎期望的閉包是可能的。
程序2-16. 使用匿名函數激發一個創建多個閉包函數所需的作用域的例子
//id為"main"的一個元素
var obj = document.getElementById("main");
//用來綁定的items數組
var items = [ "click", "keypress" ];
//遍曆items中的每一項
for ( var i = 0; i < items.length; i++ ) {
//用自執行的匿名函數來激發作用域
(function(){
//在些作用域內存儲值
var item = items[i];
//為obj元素綁定函數
obj[ "on" + item ] = function() {
//item引用一個父級的變量,
//該變量在此for循環的上文中已被成功地scoped(?)
alert( "Thanks for your " + item );
};
})();
}
閉包的概念並非輕易可以掌握的;我著實花了大量的時間和精力才徹底弄清閉包有多麼強大。幸運的是,有一個精彩的資源解釋了JavaScript中的閉包是怎麼工作的:Jim Jey的"JavaScript閉包",網址是
https://jibbering.com/faq/faq_notes/closures.html。
最後,我們將研究上下文的概念,這是許多JavaScript的麵向對象特性賴以建立的基石。
上下文 在JavaScript中,你的代碼將總是有著某種形式的上下文(代碼在其內部工作的對象)。這也是其它麵向對象語言所共有的功能,但它們都不如JavaScript處理得這樣極端。
上下文是通過變量this工作。變量this總是引用代碼當前所在的那個對象。記住全局對象實際上是window對象的屬性。這意味著即使是在全局上下文裏,this變量仍然引用一個對象。上下文可以成為一個強大的工具,是麵向對象代碼不可或缺的一環。程序2-17展示了一些關於上下文的簡單例子。
程序2-17. 在上下文中使用函數然後將其上下文切換到另一個變量的例子
var obj = {
yes: function(){
// this == obj
this.val = true;
},
no: function(){
this.val = false;
}
};
//我們看到,obj對象沒有"val"的屬性
alert( obj.val == null );
//我們運行yes函數,它將改變附著在obj對象的val屬性
obj.yes();
alert( obj.val == true );
//然而,我們現在讓window.no指向obj.no方法,並運行之
window.no = obj.no;
window.no();
//這導致obj對象保持不變(上下文則切換到了window對象),
alert( obj.val == true );
//而window的val屬性被更新
alert( window.val == false );
你可能已經注意到,在程序2-17中,當我們切換obj.no方法的上下文到變量window時,笨重的代碼需要切換函數的上下文。幸運的是,JavaScript提供了兩種方法使這一過程變得更加易於理解和實現。程序2-18展示了恰能些目的的兩種不同方法,call和apply。
程序2-18. 改變函數上下文的示例
//一個簡單的設置其上下文的顏色風格的函數
function changeColor( color ) {
this.style.color = color;
}
//在window對象上調用這個函數將會出錯,因為window沒有style對象
changeColor( "white" );
//得到一個id為"main"的對象
var main = document.getElementById("main");
//用call方法改變它的顏色為黑
//call方法將第一個參數設置為上下文,
//並其它所有參數傳遞給函數
changeColor.call( main, "black" );
//一個設置body元素的顏色的函數
function setBodyColor() {
//apply方法設置上下文為body元素
//第一個參數為設置的上下文,
//第二個參數是一個被作為參數傳遞給函數的數組
// of arguments that gets passed to the function
changeColor.apply( document.body, arguments );
}
//設置body元素的顏色為黑
setBodyColor( "black" );
上下文的有用性此處可能還沒有立即顯現。當我們進入下一節"麵向對象的JavaScript"時,它會變得更加明顯。
[
本帖最後由 mozart0 於 2007-4-8 12:41 編輯 ]