原始值和引用值
在ECMAScript中,變數可以存放兩種型別的值,即原始值和引用值。
- 原始值(primitive value)是儲存在棧(stack)中的簡單資料段,也就是說,它們的值直接儲存在變數訪問的位置。
- 引用值(reference value)是儲存在堆(heap)中的物件,也就是說,儲存在變數處的值是一個指標(point),指向儲存物件的記憶體處。
為變數賦值時,ECMAScript的解釋程式必須判斷該值是原始型別的,還是引用型別的。要實現這一點,解釋程式則需嘗試判斷該值是否為ECMAScript的原始型別之一,即Undefined、Null、Boolean和String型。由於這些原始型別佔據的空間是固定的,所以可將它們儲存在較小的記憶體區域——棧中。這樣儲存便於迅速查尋變數的值。
在許多語言中,字串都被看作引用型別,而非原始型別,因為字串的長度是可變的。ECMAScript打破了這一傳統。
如果一個值是引用型別的,那麼它的儲存空間將從堆中分配。由於引用值的大小會改變,所以不能把它放在棧中,否則會降低變數查尋的速度。相反,放在變數的棧空間中的值是該物件儲存在堆中的地址。地址的大小是固定的,所以把它儲存在棧中對變數效能無任何負面影響,如圖:
堆疊就像我們的書一樣,堆對應著書的正文內容,而棧對應的是書的目錄,章節標題是簡短的,所以在目錄裡面可以放,而文章的內容則不能放在目錄裡,我們只需要在目錄放一個對應文章的標題和頁碼.這樣,即可以在一本書中放大量的內容,又因為存在目錄,可以快速查詢內容!
原始型別
如前所述,ECMAScript有5種原始型別(primitive type),即Undefined、Null、Boolean、Number和String。ECMA-262把術語型別(type)定義為值的一個集合,每種原始型別定義了它包含的值的範圍及其字面量表示形式。ECMAScript提供了typeof運算子來判斷一個值是否在某種型別的範圍內。可以用這種運算子判斷一個值是否表示一種原始型別;如果它是原始型別,還可以判斷它表示哪種原始型別。
typeof運算子有一個引數,即要檢查的變數或值。例如:
var str = "some thing"; alert(typeof str);//輸出string var num =123; alert(typeof(num));//輸出number,typeof運算子可以像函式一樣使用 //這主要運用於一些複雜的表達的以解決優先順序問題 alert(typeof num*3);//輸出NaN,因為typeof優先於* alert(typeof(num*3));//輸出number
對變數或值呼叫typeof運算子將返回下列值之一:
- "undefined",如果變數是Undefined型的
- "boolean",如果變數是Boolean型的
- "number",如果變數是Number型的
- "string",如果變數是String型的
- "object",如果變數是一種引用型別或Null型別的
你也許會問,為什麼typeof運算子對於null值會返回"object"。這實際上是JavaScript最初實現中的一個錯誤,然後被ECMAScript沿用了。現在,null被認為是物件的佔位符,從而解釋了這一矛盾,但從技術上來說,它仍然是原始值。
如前所述,Undefined型別只有一個值,即undefined。當宣告的變數未初始化時,該變數的預設值是undefined
var a; alert(a);//輸出undefined alert(typeof a);//輸出undefined alert(a==undefined);//true
注意,值undefined並不同於未定義的值。但是,typeof運算子並不真正區分這兩種值。考慮下面的程式碼:
var tmp; alert(tmp);//undefined alert(typeof tmp);//undefined alert(typeof tmp2);//undefined
前面的程式碼對兩個變數輸出的都是"undefined",即使只有變數tmp2是未定義的。如果不用typeof運算子,就對tmp2使用其他運算子,這將引起錯誤,因為那些運算子只能用於已定義的變數。例如,下面的程式碼將引發錯誤:
alert(tmp3==undefined);//出錯,因為變數tmp3未宣告
當函式無明確返回值時,返回的也是值undefined,如下所示:
function tmp() {}//空函式,沒有返回值 alert(tmp());//undefined
另一種只有一個值的型別是Null,它只有一個專用值null,即它的字面量。值undefined實際上是從值null派生來的,因此ECMAScript把它們定義為相等的。但它們並不是嚴格相等的,因為它們是不同型別!
alert(null==undefined);//輸出true
儘管這兩個值相等,但它們的含義不同。undefined是宣告瞭變數但未對其初始化時賦予該變數的值,null則用於表示尚未存在的物件。如果函式或方法要返回的是物件,那麼找不到該物件時,返回的通常是null。
var obj = document.getElementById("tmp");//頁面上沒有ID為tmp的元素 alert(obj);//null
Boolean型別是ECMAScript中最常用的型別之一。它有兩個值true和false(即兩個Boolean字面量)。即使false不等於0,0也可以在必要時被轉換成false,這樣在Boolean語句中使用兩者都是安全的。
var bool=true; bool=false;
ECMA-262中定義的最特殊的型別是Number型。這種型別既可以表示32位的整數,還可以表示64位的浮點數。直接輸入的(而不是從另一個變數訪問的)任何數字都被看作Number型的字面量。例如,下面的程式碼宣告瞭存放整數值的變數,它的值由字面量55定義:
var num =55;
整數也可以被表示為八進位制(以8為底)或十六進位制(以16為底)的字面量。八進位制字面量的首數字必須是0,其後的數字可以是任何八進位制數字(0到7),如下面程式碼所示:
var num=020;//八進位制10為十進位制的16 alert(num);//雖然我們用八進位制方法表示一個數,但輸出時,系統會自動輸出它的十進位制表現形式
要建立十六進位制的字面量,首位數字必須為0,其後接字母x,然後是任意的十六進位制數字(0到9和A到F)。這些字母可以是大寫的,也可以是小寫的。例如:
var num=0x12;//十進位制18 num=0xab;//171 alert(num);//輸出的仍是十進位制的171
注意!儘管所有整數都可表示為八進位制或十六進位制的字面量,但所有數學運算返回的都是十進位制結果!
要定義浮點值,必須包括小數點和小數點後的一位數字(例如,用1.0而不是1)。這被看作浮點數字面量。例如:
var num = 1;//整數 var num2=1.0;//浮點數
對於非常大或非常小的數,可以用科學記數法表示浮點值。採用科學記數法,可以把一個數表示為數字(包括十進位制數字)加e(或E),後面加乘以10的倍數。不明白?下面是一個示例:
var num = 3E10; alert(num);//30000000000 num=3.45E2; alert(num);//345
也可用科學記數法表示非常小的數,例如0.00000000000000003可以表示為3-e17(這裡,10被升到-17次冪,意味著需要被10除17次)。ECMAScript預設把具有6個或6個以上前導0的浮點數轉換成科學記數法。
幾個特殊值也被定義為Number型別的。前兩個是Number.MAX_VALUE和Number.MIN_ VALUE,它們定義了Number值集合的外邊界。所有ECMAScript數都必須在這兩個值之間。不過計算生成的數值結果可以不落在這兩個數之間。當計算生成的數大於Number.MAX_VALUE時,它將被賦予值Number.POSITIVE_INFINITY,意味著不再有數字值。同樣,生成的數值小於Number.MIN_VALUE的計算也會被賦予值Number.NEGATIVE_INFINITY,也意味著不再有數字值。如果計算返回的是無窮大值,那麼生成的結果不能再用於其他計算。
事實上,有專門的值表示無窮大,即Infinity。Number.POSITIVE_INFINITY的值為Infinity,Number.NEGATIVE_INFINITY的值為-Infinity。
alert(Number.MAX_VALUE*2 == Infinity);//true alert(Number.NEGATIVE_INFINITY == -Infinity);//true
由於無窮大數可以是正數也可以是負數,所以可用一個方法判斷一個數是否是有窮的(而不是單獨測試每個無窮數)。可以對任何數呼叫isFinit()方法,以確保該數不是無窮大。例如:
var a = 9E9999999999999999999999999999999999;//已經超過範圍了 if (isFinite(a)) { alert("一個有窮數!"); } else { alert("一個無窮數!"); }
最後一個特殊值是NaN,表示非數(Not a Number)。NaN是個奇怪的特殊值。一般說來,這種情況發生在型別(String、Boolean等)轉換失敗時。例如,要把單詞blue轉換成數值就會失敗,因為沒有與之等價的數值。與無窮大值一樣,NaN也不能用於算術計算。NaN的另一個奇特之處在於,它與自身不相等,這意味著下面的程式碼將返回false:
alert(NaN); alert(!!NaN); alert(NaN == NaN);
出於這種原因,不推薦使用NaN值本身。函式isNaN()會做得相當好:
alert(isNaN(NaN));//true alert(isNaN(123));//false
String型別的獨特之處在於,它是唯一沒有固定大小的原始型別。可以用字串儲存0或更多的Unicode字元,由16位整數表示(Unicode是一種國際字符集,本書後面將討論它)。字串中每個字元都有特定的位置,首字元從位置0開始,第二個字元在位置1,依此類推。這意味著字串中的最後一個字元的位置一定是字串的長度減1
字串字面量是由雙引號(")或單引號(')宣告的。與Java不同的是,雙引號用於宣告字串,單引號用於宣告字元。但是,由於ECMAScript沒有字元型別,所以可使用這兩種表示法中的任何一種。
String型別還包括幾種字元字面量,Java、C和Perl的開發者應該對此非常熟悉。下表列出了ECMAScript的字元字面量:
字 面 量 | 含 義 |
---|---|
\n | 換行 |
\t | 製表符 |
\b | 空格 |
\r | 回車 |
\f | 換頁符 |
\\ | 反斜槓 |
\' | 單引號 |
\" | 雙引號 |
\0nnn | 八進位制程式碼nnn(n是0到7中的一個八進位制數字)表示的字元 |
\xnn | 十六進位制程式碼nn(n是0到F中的一個十六進位制數字)表示的字元 |
\unnnn | 十六進位制程式碼nnnn(n是0到F中的一個十六進位制數字)表示的Unicode字元 |
型別轉換
所有程式設計語言最重要的特徵之一是具有進行型別轉換的能力,ECMAScript給開發者提供了大量簡單的轉換方法。大多數型別具有進行簡單轉換的方法,還有幾個全域性方法可以用於更復雜的轉換。無論哪種情況,在ECMAScript中,型別轉換都是簡短的一步操作。
ECMAScript的Boolean值、數字和字串的原始值的有趣之處在於它們是偽物件,這意味著它們實際上具有屬性和方法。例如,要獲得字串的長度,可以採用下面的程式碼:
var str ="some"; alert(str.length);
儘管"blue"是原始型別的字串,它仍然具有屬性length,用於存放該字串的大小。總而言之,3種主要的原始值Boolean值、數字和字串都有toString()方法,可以把它們的值轉換成字串。
也許你會問,“字串還有toString()方法,這不是多餘的嗎?”是的,的確如此,不過ECMAScript定義所有物件都有toString()方法,無論它是偽物件,還是真的物件。因為String型別屬於偽物件,所以它一定有toString()方法。
Boolean型的toString()方法只是輸出"true"或"false",結果由變數的值決定:
var bool=true; alert(bool.toString());
Number型別的toString()方法比較特殊,它有兩種模式,即預設模式和基模式。採用預設模式,toString()方法只是用相應的字串輸出數字值(無論是整數、浮點數還是科學記數法),如下所示:
var num=10; alert(num.toString()); num=10.0; alert(num.toString());//仍然是10
在預設模式中,無論最初採用什麼表示法宣告數字,Number型別的toString()方法返回的都是數字的十進位制表示。因此,以八進位制或十六進位制字面量形式宣告的數字輸出時都是十進位制形式的。
採用Number型別的toString()方法的基模式,可以用不同的基輸出數字,例如二進位制的基是2,八進位制的基是8,十六進位制的基是16。基只是要轉換成的基數的另一種叫法而已,它是toString()方法的引數:
var num=10; alert(num.toString());//10 alert(num.toString(2));//1010 alert(num.toString(8));//12 alert(num.toString(16));//A //對數字呼叫toString(10)與呼叫toString()相同,它們返回的都是該數字的十進位制形式。 alert(num.toString(10));
ECMAScript提供了兩種把非數字的原始值轉換成數字的方法,即parseInt()和parseFloat()。正如你可能想到的,前者把值轉換成整數,後者把值轉換成浮點數。只有對String型別呼叫這些方法,它們才能正確執行;對其他型別返回的都是NaN。
alert(parseInt("12.23"));//12 alert(parseFloat("12.45"));//12.45 alert(parseFloat("12.23.34"));//12.23 alert(parseFloat("abc"));//NaN alert(parseInt(true));//NaN
在判斷字串是否是數字值前,parseInt()和parseFloat()都會仔細分析該字串。parseInt()方法首先檢視位置0處的字元,判斷它是否是個有效數字;如果不是,該方法將返回NaN,不再繼續執行其他操作。但如果該字元是有效數字,該方法將檢視位置1處的字元,進行同樣的測試。這一過程將持續到發現非有效數字的字元為止,此時parseInt()將把該字元之前的字串轉換成數字。例如,如果要把字串"1234blue"轉換成整數,那麼parseInt()將返回1234,因為當它檢測到字元b時,就會停止檢測過程。字串中包含的數字字面量會被正確轉換為數字,因此字串"0xA"會被正確轉換為數字10。不過,字串"22.5"將被轉換成22,因為對於整數來說,小數點是無效字元。
parseInt()方法還有基模式,可以把二進位制、八進位制、十六進位制或其他任何進位制的字串轉換成整數。基是由parseInt()方法的第二個引數指定的,所以要解析十六進位制的值,需如下呼叫parseInt()方法:
alert(parseInt("AB",16));//171 alert(parseInt("10",2));//2 alert(parseInt("10",8));//8
如果十進位制數包含前導0,那麼最好採用基數10,這樣才不會意外地得到八進位制的值。例如:
var str ="010"; alert(parseInt(str));//8 alert(parseInt(str,10));//10
parseFloat()沒有基模式
強制型別轉換
還可使用強制型別轉換(type casting)處理轉換值的型別。使用強制型別轉換可以訪問特定的值,即使它是另一種型別的。ECMAScript中可用的3種強制型別轉換如下:
- Boolean(value)——把給定的值轉換成Boolean型
- Number(value)——把給定的值轉換成數字(可以是整數或浮點數)
- String(value)——把給定的值轉換成字串
用這三個函式之一轉換值,將建立一個新值,存放由原始值直接轉換成的值。這會造成意想不到的後果。
當要轉換的值是至少有一個字元的字串、非0數字或物件時,Boolean()函式將返回true。如果該值是空字串、數字0、undefined或null,它將返回false。可以用下面的程式碼段測試Boolean型的強制型別轉換。
var b1 = Boolean(""); //false - 空字串 var b2 = Boolean("hello"); //true - 非空字串 var b1 = Boolean(50); //true - 非零數字 var b1 = Boolean(null); //false - null var b1 = Boolean(0); //false - 零 var b1 = Boolean(new object()); //true - 物件
Number() 函式的強制型別轉換與 parseInt() 和 parseFloat() 方法的處理方式相似,只是它轉換的是整個值,而不是部分值。
parseInt() 和 parseFloat() 方法只轉換第一個無效字元之前的字串,因此 "1.2.3" 將分別被轉換為 "1" 和 "1.2"。用 Number() 進行強制型別轉換,"1.2.3" 將返回 NaN,因為整個字串值不能轉換成數字。如果字串值能被完整地轉換,Number() 將判斷是呼叫 parseInt() 方法還是 parseFloat() 方法。
下表說明了對不同的值呼叫 Number() 方法會發生的情況:
用法 | 結果 |
---|---|
Number(false) | 0 |
Number(true) | 1 |
Number(undefined) | NaN |
Number(null) | 0 |
Number("1.2") | 1.2 |
Number("12") | 12 |
Number("1.2.3") | NaN |
Number(new Object()) | NaN |
Number(50) | 50 |
最後一種強制型別轉換方法 String() 是最簡單的,因為它可把任何值轉換成字串。要執行這種強制型別轉換,只需要呼叫作為引數傳遞進來的值的 toString() 方法,即把 12 轉換成 "12",把 true 轉換成 "true",把 false 轉換成 "false",以此類推。強制轉換成字串和呼叫 toString() 方法的唯一不同之處在於,對 null 和 undefined 值強制型別轉換可以生成字串而不引發錯誤:
var s1 = String(null); //"null" var oNull = null; var s2 = oNull.toString(); //會引發錯誤
在處理 ECMAScript 這樣的弱型別語言時,強制型別轉換非常有用,不過應該確保使用值的正確。