# 第二章:值
`array`、`string`、和?`number`?是任何程序的最基礎構建塊,但是 JavaScript 在這些類型上有一些或使你驚喜或使你驚訝的獨特性質。
讓我們來看幾種 JS 內建的值類型,并探討一下我們如何才能更加全面地理解并正確地利用它們的行為。
## Array
和其他強制類型的語言相比,JavaScript 的?`array`?只是值的容器,而這些值可以是任何類型:`string`?或者?`number`?或者?`object`,甚至是另一個?`array`(這也是你得到多維數組的方法)。
```source-js
var a = [ 1, "2", [3] ];
a.length; // 3
a[0] === 1; // true
a[2][0] === 3; // true
```
你不需要預先指定?`array`?的大小,你可以僅聲明它們并加入你覺得合適的值:
```source-js
var a = [ ];
a.length; // 0
a[0] = 1;
a[1] = "2";
a[2] = [ 3 ];
a.length; // 3
```
警告:?在一個?`array`?值上使用?`delete`?將會從這個?`array`?上移除一個值槽,但就算你移除了最后一個元素,它也?不會?更新?`length`?屬性,所以多加小心!我們會在第五章討論?`delete`?操作符的更多細節。
要小心創建“稀散”的?`array`(留下或創建空的/丟失的值槽):
```source-js
var a = [ ];
a[0] = 1;
// 這里沒有設置值槽 `a[1]`
a[2] = [ 3 ];
a[1]; // undefined
a.length; // 3
```
雖然這可以工作,但你留下的“空值槽”可能會導致一些令人困惑的行為。雖然這樣的值槽看起來擁有?`undefined`?值,但是它不會像被明確設置(`a[1] = undefined`)的值槽那樣動作。更多信息可以參見第三章的“Array”。
`array`?是被數字索引的(正如你所想的那樣),但微妙的是它們也是對象,可以在它們上面添加?`string`?鍵/屬性(但是這些屬性不會計算在?`array`?的?`length`?中):
```source-js
var a = [ ];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
```
然而,一個需要小心的坑是,如果一個可以被強制轉換為10進制?`number`?的?`string`?值被用作鍵的話,它會認為你想使用?`number`?索引而不是一個?`string`?鍵!
```source-js
var a = [ ];
a["13"] = 42;
a.length; // 14
```
一般來說,向?`array`?添加?`string`?鍵/屬性不是一個好主意。最好使用?`object`?來持有鍵/屬性形式的值,而將?`array`?專用于嚴格地數字索引的值。
### 類 Array
偶爾你需要將一個類?`array`?值(一個數字索引的值的集合)轉換為一個真正的?`array`,通常你可以對這些值的集合調用數組的工具函數(比如?`indexOf(..)`、`concat(..)`、`forEach(..)`?等等)。
舉個例子,各種 DOM 查詢操作會返回一個 DOM 元素的列表,對于我們轉換的目的來說,這些列表不是真正的?`array`?但是也足夠類似?`array`。另一個常見的例子是,函數為了像列表一樣訪問它的參數值,而暴露了?`arugumens`?對象(類?`array`,在 ES6 中被廢棄了)。
一個進行這種轉換的很常見的方法是對這個值借用?`slice(..)`?工具:
```source-js
function foo() {
var arr = Array.prototype.slice.call( arguments );
arr.push( "bam" );
console.log( arr );
}
foo( "bar", "baz" ); // ["bar","baz","bam"]
```
如果?`slice()`?沒有用其他額外的參數調用,就像上面的代碼段那樣,它的參數的默認值會使它具有復制這個?`array`(或者,在這個例子中,是一個類?`array`)的效果。
在 ES6 中,還有一種稱為?`Array.from(..)`?的內建工具可以執行相同的任務:
```source-js
...
var arr = Array.from( arguments );
...
```
注意:?`Array.from(..)`?擁有其他幾種強大的能力,我們將在本系列的?*ES6 與未來*?中涵蓋它的細節。
## String
一個很常見的想法是,`string`?實質上只是字符的?`array`。雖然內部的實現可能是也可能不是?`array`,但重要的是要理解 JavaScript 的?`string`?與字符的?`array`?確實不一樣。它們的相似性幾乎只是表面上的。
舉個例子,讓我們考慮這兩個值:
```source-js
var a = "foo";
var b = ["f","o","o"];
```
String 確實與?`array`?有很膚淺的相似性 -- 也就是上面說的,類?`array`?-- 舉例來說,它們都有一個?`length`?屬性,一個?`indexOf(..)`?方法(在 ES5 中僅有?`array`?版本),和一個?`concat(..)`?方法:
```source-js
a.length; // 3
b.length; // 3
a.indexOf( "o" ); // 1
b.indexOf( "o" ); // 1
var c = a.concat( "bar" ); // "foobar"
var d = b.concat( ["b","a","r"] ); // ["f","o","o","b","a","r"]
a === c; // false
b === d; // false
a; // "foo"
b; // ["f","o","o"]
```
那么,它們基本上都僅僅是“字符的數組”,對吧??不確切:
```source-js
a[1] = "O";
b[1] = "O";
a; // "foo"
b; // ["f","O","o"]
```
JavaScript 的?`string`?是不可變的,而?`array`?是相當可變的。另外,在 JavaScript 中用位置訪問字符的?`a[1]`?形式不總是廣泛合法的。老版本的 IE 就不允許這種語法(但是它們現在允許了)。相反,*正確的*?方式是?`a.charAt(1)`。
`string`?不可變性的進一步的后果是,`string`?上沒有一個方法是可以原地修改它的內容的,而是創建并返回一個新的?`string`。與之相對的是,許多改變?`array`?內容的方法實際上?*是*?原地修改的。
```source-js
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push( "!" );
b; // ["f","O","o","!"]
```
另外,許多?`array`?方法在處理?`string`?時非常有用,雖然這些方法不屬于?`string`,但我們可以對我們的?`string`?“借用”非變化的?`array`?方法:
```source-js
a.join; // undefined
a.map; // undefined
var c = Array.prototype.join.call( a, "-" );
var d = Array.prototype.map.call( a, function(v){
return v.toUpperCase() + ".";
} ).join( "" );
c; // "f-o-o"
d; // "F.O.O."
```
讓我們來看另一個例子:翻轉一個?`string`(順帶一提,這是一個 JavaScript 面試中常見的細節問題!)。`array`?擁有一個原地的?`reverse()`?修改器方法,但是?`string`?沒有:
```source-js
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["!","o","O","f"]
```
不幸的是,這種“借用”?`array`?修改器不起作用,因為?`string`?是不可變的,因此它不能被原地修改:
```source-js
Array.prototype.reverse.call( a );
// 仍然返回一個“foo”的 String 對象包裝器(見第三章) :(
```
另一種迂回的做法(也是黑科技)是,將?`string`?轉換為一個?`array`,實施我們想做的操作,然后將它轉回?`string`。
```source-js
var c = a
// 將 `a` 切分成一個字符的數組
.split( "" )
// 翻轉字符的數組
.reverse()
// 將字符的數組連接回一個字符串
.join( "" );
c; // "oof"
```
如果你覺得這很難看,沒錯。不管怎樣,對于簡單的?`string`?它?*好用*,所以如果你需要某些快速但是“臟”的東西,像這樣的方式經常能滿足你。
警告:?小心!這種方法對含有復雜(unicode)字符(星型字符、多字節字符等)的?`string`?不起作用。你需要支持 unicode 的更精巧的工具庫來準確地處理這種操作。在這個問題上可以咨詢 Mathias Bynens 的作品:*Esrever*([https://github.com/mathiasbynens/esrever)。](https://github.com/mathiasbynens/esrever%EF%BC%89%E3%80%82)
另外一種考慮這個問題的方式是:如果你更經常地將你的“string”基本上作為?*字符的數組*?來執行一些任務的話,也許就將它們作為?`array`?而不是作為?`string`?存儲更好。你可能會因此省去很多每次都將?`string`?轉換為?`array`?的麻煩。無論何時你確實需要?`string`?的表現形式的話,你總是可以調用?*字符的*?`array`?的?`join("")`?方法。
## Number
JavaScript 只有一種數字類型:`number`。這種類型包含“整數”值和小數值。我說“整數”時加了引號,因為 JS 的一個長久以來為人詬病的原因是,和其他語言不同,JS 沒有真正的整數。這可能在未來某個時候會改變,但是目前,我們只有?`number`?可用。
所以,在 JS 中,一個“整數”只是一個沒有小數部分的小數值。也就是說,`42.0`?和?`42`?一樣是“整數”。
像大多數現代計算機語言,以及幾乎所有的腳本語言一樣,JavaScript 的?`number`?的實現基于“IEEE 754”標準,通常被稱為“浮點”。JavaScript 明確地使用了這個標準的“雙精度”(也就是“64位二進制”)格式。
在網絡上有許多了不起的文章都在介紹二進制浮點數如何在內存中存儲的細節,以及選擇這些做法的意義。因為對于理解如何在 JS 中正確使用?`number`?來說,理解內存中的位模式不是必須的,所以我們將這個話題作為練習留給那些想要進一步挖掘 IEEE 754 的細節的讀者。
### 數字的語法
在 JavaScript 中字面數字一般用十進制小數表達。例如:
```source-js
var a = 42;
var b = 42.3;
```
小數的整數部分如果是?`0`,是可選的:
```source-js
var a = 0.42;
var b = .42;
```
相似地,一個小數在?`.`?之后的小數部分如果是?`0`,是可選的:
```source-js
var a = 42.0;
var b = 42.;
```
警告:?`42.`?是極不常見的,如果你正在努力避免別人閱讀你的代碼時感到困惑,它可能不是一個好主意。但不管怎樣,它是合法的。
默認情況下,大多數?`number`?將會以十進制小數的形式輸出,并去掉末尾小數部分的?`0`。所以:
```source-js
var a = 42.300;
var b = 42.0;
a; // 42.3
b; // 42
```
非常大或非常小的?`number`?將默認以指數形式輸出,與?`toExponential()`?方法的輸出一樣,比如:
```source-js
var a = 5E10;
a; // 50000000000
a.toExponential(); // "5e+10"
var b = a * a;
b; // 2.5e+21
var c = 1 / a;
c; // 2e-11
```
因為?`number`?值可以用?`Number`?對象包裝器封裝(見第三章),所以?`number`?值可以訪問內建在?`Number.prototype`?上的方法(見第三章)。舉個例子,`toFixed(..)`?方法允許你指定一個值在被表示時,帶有多少位小數:
```source-js
var a = 42.59;
a.toFixed( 0 ); // "43"
a.toFixed( 1 ); // "42.6"
a.toFixed( 2 ); // "42.59"
a.toFixed( 3 ); // "42.590"
a.toFixed( 4 ); // "42.5900"
```
要注意的是,它的輸出實際上是一個?`number`?的?`string`?表現形式,而且如果你指定的位數多于值持有的小數位數時,會在右側補?`0`。
`toPrecision(..)`?很相似,但它指定的是有多少?*有效數字*?用來表示這個值:
```source-js
var a = 42.59;
a.toPrecision( 1 ); // "4e+1"
a.toPrecision( 2 ); // "43"
a.toPrecision( 3 ); // "42.6"
a.toPrecision( 4 ); // "42.59"
a.toPrecision( 5 ); // "42.590"
a.toPrecision( 6 ); // "42.5900"
```
你不必非得使用持有這個值的變量來訪問這些方法;你可以直接在?`number`?的字面上訪問這些方法。但你不得不小心?`.`?操作符。因為?`.`?是一個合法數字字符,如果可能的話,它會首先被翻譯為?`number`?字面的一部分,而不是被翻譯為屬性訪問操作符。
```source-js
// 不合法的語法:
42.toFixed( 3 ); // SyntaxError
// 這些都是合法的:
(42).toFixed( 3 ); // "42.000"
0.42.toFixed( 3 ); // "0.420"
42..toFixed( 3 ); // "42.000"
```
`42.toFixed(3)`?是不合法的語法,因為?`.`?作為?`42.`?字面(這是合法的 -- 參見上面的討論!)的一部分被吞掉了,因此沒有?`.`?屬性操作符來表示?`.toFixed`?訪問。
`42..toFixed(3)`?可以工作,因為第一個?`.`?是?`number`?的一部分,而第二個?`.`?是屬性操作符。但它可能看起來很古怪,而且確實在實際的 JavaScript 代碼中很少會看到這樣的東西。實際上,在任何基本類型上直接訪問方法是十分不常見的。但是不常見并不意味著?*壞*?或者?*錯*。
注意:?有一些庫擴展了內建的?`Number.prototype`(見第三章),使用?`number`?或在?`number`?上提供了額外的操作,所以在這些情況下,像使用?`10..makeItRain()`?來設定一個十秒鐘的下錢雨的動畫,或者其他諸如此類的傻事是完全合法的。
在技術上講,這也是合法的(注意那個空格):
```source-js
42 .toFixed(3); // "42.000"
```
但是,尤其是對?`number`?字面量來說,這是特別使人糊涂的代碼風格,而且除了使其他開發者(和未來的你)糊涂以外沒有任何用處。避免它。
`number`?還可以使用科學計數法的形式指定,這在表示很大的?`number`?時很常見,比如:
```source-js
var onethousand = 1E3; // 代表 1 * 10^3
var onemilliononehundredthousand = 1.1E6; // 代表 1.1 * 10^6
```
`number`?字面量還可以使用其他進制表達,比如二進制,八進制,和十六進制。
這些格式是可以在當前版本的 JavaScript 中使用的:
```source-js
0xf3; // 十六進制的: 243
0Xf3; // 同上
0363; // 八進制的: 243
```
注意:?從 ES6 +?`strict`?模式開始,不再允許?`0363`?這樣的的八進制形式(新的形式參見后面的討論)。`0363`?在非?`strict`模式下依然是允許的,但是不管怎樣你應當停止使用它,來擁抱未來(而且因為你現在應當在使用?`strict`?模式了!)。
至于 ES6,下面的新形式也是合法的:
```source-js
0o363; // 八進制的: 243
0O363; // 同上
0b11110011; // 二進制的: 243
0B11110011; // 同上
```
請為你的開發者同胞們做件好事:絕不要使用?`0O363`?形式。把?`0`?放在大寫的?`O`?旁邊就是在制造困惑。保持使用小寫的謂詞?`0x`、`0b`、和`0o`。
### 小數值
使用二進制浮點數的最出名(臭名昭著)的副作用是(記住,這是對?所有?使用 IEEE 754 的語言都成立的 —— 不是許多人認為/假裝?*僅*?在 JavaScript 中存在的問題):
```source-js
0.1 + 0.2 === 0.3; // false
```
從數學的意義上,我們知道這個語句應當為?`true`。為什么它是?`false`?
簡單地說,`0.1`?和?`0.2`?的二進制表示形式是不精確的,所以它們相加時,結果不是精確地?`0.3`。而是?非常?接近的值:`0.30000000000000004`,但是如果你的比較失敗了,“接近”是無關緊要的。
注意:?JavaScript 應當切換到可以精確表達所有值的一個不同的?`number`?實現嗎?有些人認為應該。多年以來有許多選項出現過。但是沒有一個被采納,而且也許永遠也不會。它看起來就像揮揮手然后說“已經改好那個 bug 了!”那么簡單,但根本不是那么回事兒。如果真有這么簡單,它絕對在很久以前就被改掉了。
現在的問題是,如果一些?`number`?不能被?*信任*?為精確的,這不是意味著我們根本不能使用?`number`?嗎??當然不是。
在一些應用程序中你需要多加小心,特別是在對付小數的時候。還有許多(也許是大多數?)應用程序只處理整數,而且,最大只處理到幾百萬到幾萬億。這些應用程序使用 JS 中的數字操作是,而且將總是,非常安全?的。
要是我們?*確實*?需要比較兩個?`number`,就像?`0.1 + 0.2`?與?`0.3`,而且知道這個簡單的相等測試將會失敗呢?
可以接受的最常見的做法是使用一個很小的“錯誤舍入”值作為比較的?*容差*。這個很小的值經常被稱為“機械極小值(machine epsilon)”,對于 JavaScript 來說這種?`number`?通常為?`2^-52`(`2.220446049250313e-16`)。
在 ES6 中,使用這個容差值預定義了?`Number.EPSILON`,所以你將會想要使用它,你也可以在前 ES6 中安全地填補這個定義:
```source-js
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
```
我們可以使用這個?`Number.EPSILON`?來比較兩個?`number`?的“等價性”(帶有錯誤舍入的容差):
```source-js
function numbersCloseEnoughToEqual(n1,n2) {
return Math.abs( n1 - n2 ) < Number.EPSILON;
}
var a = 0.1 + 0.2;
var b = 0.3;
numbersCloseEnoughToEqual( a, b ); // true
numbersCloseEnoughToEqual( 0.0000001, 0.0000002 ); // false
```
可以被表示的最大的浮點值大概是?`1.798e+308`(它真的非常,非常,非常大!),它為你預定義為?`Number.MAX_VALUE`。在極小的一端,`Number.MIN_VALUE`?大概是?`5e-324`,它不是負數但是非常接近于0!
### 安全整數范圍
由于?`number`?的表示方式,對完全是?`number`?的“整數”而言有一個“安全”的值的范圍,而且它要比?`Number.MAX_VALUE`?小得多。
可以“安全地”被表示的最大整數(也就是說,可以保證被表示的值是實際可以無誤地表示的)是`2^53 - 1`,也就是`9007199254740991`,如果你插入一些數字分隔符,可以看到它剛好超過9萬億。所以對于`number`能表示的上限來說它確實是夠TM大的。
在ES6中這個值實際上是自動預定義的,它是`Number.MAX_SAFE_INTEGER`。意料之中的是,還有一個最小值,`-9007199254740991`,它在ES6中定義為`Number.MIN_SAFE_INTEGER`。
JS 程序面臨處理這樣大的數字的主要情況是,處理數據庫中的64位 ID 等等。64位數字不能使用?`number`?類型準確表達,所以在 JavaScript 中必須使用?`string`?表現形式存儲(和傳遞)。
謝天謝地,在這樣的大 ID?`number`?值上的數字操作(除了比較,它使用?`string`?也沒問題)并不很常見。但是如果你?*確實*?需要在這些非常大的值上實施數學操作,目前來講你需要使用一個?*大數字*?工具。在未來版本的 JavaScript 中,大數字也許會得到官方支持。
### 測試整數
測試一個值是否是整數,你可以使用 ES6 定義的?`Number.isInteger(..)`:
```source-js
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
```
可以為前 ES6 填補?`Number.isInteger(..)`:
```source-js
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
```
要測試一個值是否是?*安全整數*,使用 ES6 定義的?`Number.isSafeInteger(..)`:
```source-js
Number.isSafeInteger( Number.MAX_SAFE_INTEGER ); // true
Number.isSafeInteger( Math.pow( 2, 53 ) ); // false
Number.isSafeInteger( Math.pow( 2, 53 ) - 1 ); // true
```
可以為前 ES6 瀏覽器填補?`Number.isSafeInteger(..)`:
```source-js
if (!Number.isSafeInteger) {
Number.isSafeInteger = function(num) {
return Number.isInteger( num ) &&
Math.abs( num ) <= Number.MAX_SAFE_INTEGER;
};
}
```
### 32位(有符號)整數
雖然整數可以安全地最大達到約九萬億(53比特),但有一些數字操作(比如位操作符)是僅僅為32位?`number`?定義的,所以對于被這樣使用的?`number`?來說,“安全范圍”一定會小得多。
這個范圍是從?`Math.pow(-2,31)`(`-2147483648`,大約-21億)到?`Math.pow(2,31)-1`(`2147483647`,大約+21億)。
要強制?`a`?中的?`number`?值是32位有符號整數,使用?`a | 0`。這可以工作是因為?`|`?位操作符僅僅對32位值起作用(意味著它可以只關注32位,而其他的位將被丟掉)。而且,和 0 進行“或”的位操作實質上是什么也不做。
注意:?特定的特殊值(我們將在下一節討論),比如?`NaN`?和?`Infinity`?不是“32位安全”的,當這些值被傳入位操作符時將會通過一個抽象操作?`ToInt32`(見第四章)并為了位操作而簡單地變成?`+0`?值。
## 特殊值
在各種類型中散布著一些特殊值,需要?*警惕*?的 JS 開發者小心,并正確使用。
### 不是值的值
對于?`undefined`?類型來說,有且僅有一個值:`undefined`。對于?`null`?類型來說,有且僅有一個值:`null`。所以對它們而言,這些文字既是它們的類型也是它們的值。
`undefined`?和?`null`?作為“空”值或者“沒有”值,經常被認為是可以互換的。另一些開發者偏好于使用微妙的區別將它們區分開。舉例來講:
* `null`?是一個空值
* `undefined`?是一個丟失的值
或者:
* `undefined`?還沒有值
* `null`?曾經有過值但現在沒有
不管你選擇如何“定義”和使用這兩個值,`null`?是一個特殊的關鍵字,不是一個標識符,因此你不能將它作為一個變量對待來給它賦值(為什么你要給它賦值呢?!)。然而,`undefined`(不幸地)*是*?一個標識符。噢。
### Undefined
在非?`strict`?模式下,給在全局上提供的?`undefined`?標識符賦一個值實際上是可能的(雖然這是一個非常不好的做法!):
```source-js
function foo() {
undefined = 2; // 非常差勁兒的主意!
}
foo();
```
```source-js
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
```
但是,在非?`strict`?模式和?`strict`?模式下,你可以創建一個名叫?`undefined`?局部變量。但這又是一個很差勁兒的主意!
```source-js
function foo() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
foo();
```
朋友永遠不讓朋友覆蓋?`undefined`。
#### `void`?操作符
雖然?`undefined`?是一個持有內建的值?`undefined`?的內建標識符(除非被修改 —— 見上面的討論!),另一個得到這個值的方法是?`void`?操作符。
表達式?`void __`?會“躲開”任何值,所以這個表達式的結果總是值?`undefined`。它不會修改任何已經存在的值;只是確保不會有值從操作符表達式中返回來。
```source-js
var a = 42;
console.log( void a, a ); // undefined 42
```
從慣例上講(大約是從 C 語言編程中發展而來),要通過使用?`void`?來獨立表現值?`undefined`,你可以使用?`void 0`(雖然,很明顯,`void true`?或者任何其他的?`void`?表達式都做同樣的事情)。在?`void 0`、`void 1`?和?`undefined`?之間沒有實際上的區別。
但是在幾種其他的環境下?`void`?操作符可以十分有用:如果你需要確保一個表達式沒有結果值(即便它有副作用)。
舉個例子:
```source-js
function doSomething() {
// 注意:`APP.ready` 是由我們的應用程序提供的
if (!APP.ready) {
// 稍后再試一次
return void setTimeout( doSomething, 100 );
}
var result;
// 做其他一些事情
return result;
}
// 我們能立即執行嗎?
if (doSomething()) {
// 馬上處理其他任務
}
```
這里,`setTimeout(..)`?函數返回一個數字值(時間間隔定時器的唯一標識符,用于取消它自己),但是我們想?`void`?它,這樣我們函數的返回值不會在?`if`?語句上給出一個成立的誤報。
許多開發者寧愿將這些動作分開,這樣的效用相同但不使用?`void`?操作符:
```source-js
if (!APP.ready) {
// 稍后再試一次
setTimeout( doSomething, 100 );
return;
}
```
一般來說,如果有那么一個地方,有一個值存在(來自某個表達式)而你發現這個值如果是?`undefined`?才有用,就使用?`void`操作符。這可能在你的程序中不是非常常見,但如果在一些稀有的情況下你需要它,它就十分有用。
### 特殊的數字
`number`?類型包含幾種特殊值。我們將會仔細考察每一種。
#### 不是數字的數字
如果你不使用同為?`number`(或者可以被翻譯為十進制或十六進制的普通?`number`?的值)的兩個操作數進行任何算數操作,那么操作的結果將失敗而產生一個不合法的?`number`,在這種情況下你將得到?`NaN`?值。
`NaN`?在字面上代表“不是一個?`number`(Not a Number)”,但是正如我們即將看到的,這種文字描述十分失敗而且容易誤導人。將?`NaN`?考慮為“不合法數字”,“失敗的數字”,甚至是“壞掉的數字”都要比“不是一個數字”準確得多。
舉例來說:
```source-js
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
```
換句話說:“‘不是一個數字’的類型是‘數字’”!為這使人糊涂的名字和語義歡呼吧。
`NaN`?是一種“哨兵值”(一個被賦予了特殊意義的普通的值),它代表?`number`?集合內的一種特殊的錯誤情況。這種錯誤情況實質上是:“我試著進行數學操作但是失敗了,而這就是失敗的?`number`?結果。”
那么,如果你有一個值存在某個變量中,而且你想要測試它是否是這個特殊的失敗數字?`NaN`,你也許認為你可以直接將它與?`NaN`?本身比較,就像你能對其它的值做的那樣,比如?`null`?和?`undefined`。不是這樣。
```source-js
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
```
`NaN`?是一個非常特殊的值,它從來不會等于另一個?`NaN`?值(也就是,它從來不等于它自己)。實際上,它是唯一一個不具有反射性的值(沒有恒等性?`x === x`)。所以,`NaN !== NaN`。有點奇怪,對吧?
那么,如果不能與?`NaN`?進行比較(因為這種比較將總是失敗),我們該如何測試它呢?
```source-js
var a = 2 / "foo";
isNaN( a ); // true
```
夠簡單的吧?我們使用稱為?`isNaN(..)`?的內建全局工具,它告訴我們這個值是否是?`NaN`。問題解決了!
別高興得太早。
`isNaN(..)`?工具有一個重大缺陷。它似乎過于按照字面的意思(“不是一個數字”)去理解?`NaN`?的含義了 —— 它的工作基本上是:“測試這個傳進來的東西是否不是一個?`number`?或者是一個?`number`”。但這不是十分準確。
```source-js
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; // "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true -- 噢!
```
很明顯,`"foo"`?根本?*不是一個?`number`*,但它也絕不是一個?`NaN`?值!這個 bug 從最開始的時候就存在于 JS 中了(存在超過了十九年的坑)。
在 ES6 中,終于提供了一個替代它的工具:`Number.isNaN(..)`。有一個簡單的填補,可以讓你即使是在前 ES6 的瀏覽器中安全地檢查?`NaN`?值:
```source-js
if (!Number.isNaN) {
Number.isNaN = function(n) {
return (
typeof n === "number" &&
window.isNaN( n )
);
};
}
var a = 2 / "foo";
var b = "foo";
Number.isNaN( a ); // true
Number.isNaN( b ); // false -- 咻!
```
實際上,通過利用?`NaN`?與它自己不相等這個特殊的事實,我們可以更簡單地實現?`Number.isNaN(..)`?的填補。在整個語言中?`NaN`?是唯一一個這樣的值;其他的值都總是?等于它自己。
所以:
```source-js
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
```
怪吧?但是好用!
不管有意還是無意,在許多真實世界的 JS 程序中?`NaN`?可能是一個現實的問題。使用?`Number.isNaN(..)`(或者它的填補)這樣的可靠測試來正確地識別它們是一個非常好的主意。
如果你正在程序中僅使用?`isNaN(..)`,悲慘的現實是你的程序?*有 bug*,即便是你還沒有被它咬到!
#### 無窮
來自于像 C 這樣的傳統編譯型語言的開發者,可能習慣于看到編譯器錯誤或者是運行時異常,比如對這樣一個操作給出的“除數為 0”:
```source-js
var a = 1 / 0;
```
然而在 JS 中,這個操作是明確定義的,而且它的結果是值?`Infinity`(也就是?`Number.POSITIVE_INFINITY`)。意料之中的是:
```source-js
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
```
如你所見,`-Infinity`(也就是?`Number.NEGATIVE_INFINITY`)是從任一個被除數為負(不是兩個都是負數!)的除 0 操作得來的。
JS 使用有限的數字表現形式(IEEE 754 浮點,我們早先討論過),所以和單純的數學相比,它看起來甚至在做加法和減法這樣的操作時都有可能溢出,這樣的情況下你將會得到?`Infinity`?或?`-Infinity`。
例如:
```source-js
var a = Number.MAX_VALUE; // 1.7976931348623157e+308
a + a; // Infinity
a + Math.pow( 2, 970 ); // Infinity
a + Math.pow( 2, 969 ); // 1.7976931348623157e+308
```
根據語言規范,如果一個像加法這樣的操作得到一個太大而不能表示的值,IEEE 754 “就近舍入”模式將會指明結果應該是什么。所以粗略的意義上,`Number.MAX_VALUE + Math.pow( 2, 969 )`?比起?`Infinity`?更接近于?`Number.MAX_VALUE`,所以它“向下舍入”,而?`Number.MAX_VALUE + Math.pow( 2, 970 )`?距離?`Infinity`?更近,所以它“向上舍入”。
如果你對此考慮的太多,它會使你頭疼的。所以別想了。我是認真的,停!
一旦你溢出了任意一個?*無限值*,那么,就沒有回頭路了。換句最有詩意的話說,你可以從有限邁向無限,但不能從無限回歸有限。
“無限除以無限等于什么”,這簡直是一個哲學問題。我們幼稚的大腦可能會說“1”或“無限”。事實表明它們都不對。在數學上和在 JavaScript 中,`Infinity / Infinity`?不是一個有定義的操作。在 JS 中,它的結果為?`NaN`。
一個有限的正?`number`?除以?`Infinity`?呢?簡單!`0`。那一個有限的負?`number`?處理?`Infinity`?呢?接著往下讀!
#### 零
雖然這可能使有數學頭腦的讀者困惑,但 JavaScript 擁有普通的零?`0`(也稱為正零?`+0`)?*和*?一個負零?`-0`。在我們講解為什么?`-0`?存在之前,我們應該考察 JS 如何處理它,因為它可能十分令人困惑。
除了使用字面量?`-0`?指定,負的零還可以從特定的數學操作中得出。比如:
```source-js
var a = 0 / -3; // -0
var b = 0 * -3; // -0
```
加法和減法無法得出負零。
在開發者控制臺中考察一個負的零,經常顯示為?`-0`,然而直到最近這才是一個常見情況,所以一些你可能遇到的老版本瀏覽器也許依然將它報告為?`0`。
但是根據語言規范,如果你試著將一個負零轉換為字符串,它將總會被報告為?`"0"`。
```source-js
var a = 0 / -3;
// 至少(有些瀏覽器)控制臺是對的
a; // -0
// 但是語言規范堅持要向你撒謊!
a.toString(); // "0"
a + ""; // "0"
String( a ); // "0"
// 奇怪的是,就連 JSON 也加入了騙局之中
JSON.stringify( a ); // "0"
```
有趣的是,反向操作(從?`string`?到?`number`)不會撒謊:
```source-js
+"-0"; // -0
Number( "-0" ); // -0
JSON.parse( "-0" ); // -0
```
警告:?當你觀察的時候,`JSON.stringify( -0 )`?產生?`"0"`?顯得特別奇怪,因為它與反向操作不符:`JSON.parse( "-0" )`?將像你期望地那樣報告`-0`。
除了一個負零的字符串化會欺騙性地隱藏它實際的值外,比較操作符也被設定為(有意地)?*要說謊*。
```source-js
var a = 0;
var b = 0 / -3;
a == b; // true
-0 == 0; // true
a === b; // true
-0 === 0; // true
0 > -0; // false
a > b; // false
```
很明顯,如果你想在你的代碼中區分?`-0`?和?`0`,你就不能僅依靠開發者控制臺的輸出,你必須更聰明一些:
```source-js
function isNegZero(n) {
n = Number( n );
return (n === 0) && (1 / n === -Infinity);
}
isNegZero( -0 ); // true
isNegZero( 0 / -3 ); // true
isNegZero( 0 ); // false
```
那么,除了學院派的細節以外,我們為什么需要一個負零呢?
在一些應用程序中,開發者使用值的大小來表示一部分信息(比如動畫中每一幀的速度),而這個?`number`?的符號來表示另一部分信息(比如移動的方向)。
在這些應用程序中,舉例來說,如果一個變量的值變成了 0,而它丟失了符號,那么你就丟失了它是從哪個方向移動到 0 的信息。保留零的符號避免了潛在的意外信息丟失。
### 特殊等價
正如我們上面看到的,當使用等價性比較時,值?`NaN`?和值?`-0`?擁有特殊的行為。`NaN`?永遠不會和自己相等,所以你不得不使用 ES6 的?`Number.isNaN(..)`(或者它的填補)。相似地,`-0`?撒謊并假裝它和普通的正零相等(即使使用?`===`?嚴格等價 —— 見第四章),所以你不得不使用我們上面建議的某些?`isNegZero(..)`?黑科技工具。
在 ES6 中,有一個新工具可以用于測試兩個值的絕對等價性,而沒有任何這些例外。它稱為?`Object.is(..)`:
```source-js
var a = 2 / "foo";
var b = -3 * 0;
Object.is( a, NaN ); // true
Object.is( b, -0 ); // true
Object.is( b, 0 ); // false
```
對于前 ES6 環境,這是一個相當簡單的?`Object.is(..)`?填補:
```source-js
if (!Object.is) {
Object.is = function(v1, v2) {
// 測試 `-0`
if (v1 === 0 && v2 === 0) {
return 1 / v1 === 1 / v2;
}
// 測試 `NaN`
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情況
return v1 === v2;
};
}
```
`Object.is(..)`?可能不應當用于那些?`==`?或?`===`?已知?*安全*?的情況(見第四章“強制轉換”),因為這些操作符可能高效得多,并且更慣用/常見。`Object.is(..)`?很大程度上是為這些特殊的等價情況準備的。
## 值與引用
在其他許多語言中,根據你使用的語法,值可以通過值拷貝,也可以通過引用拷貝來賦予/傳遞。
比如,在 C++ 中如果你想要把一個?`number`?變量傳遞進一個函數,并使這個變量的值被更新,你可以用?`int& myNum`?這樣的東西來聲明函數參數,當你傳入一個變量?`x`?時,`myNum`?將是一個?指向?`x`?的引用;引用就像一個特殊形式的指針,你得到的是一個指向另一個變量的指針(像一個?*別名(alias)*) 。如果你沒有聲明一個引用參數,被傳入的值將?*總是*?被拷貝的,就算它是一個復雜的對象。
在 JavaScript 中,沒有指針,并且引用的工作方式有一點兒不同。你不能擁有一個從一個 JS 變量到另一個 JS 變量的引用。這是完全不可能的。
JS 中的引用指向一個(共享的)?值,所以如果你有十個不同的引用,它們都總是同一個共享值的不同引用;它們沒有一個是另一個的引用/指針。
另外,在 JavaScript 中,沒有語法上的提示可以控制值和引用的賦值/傳遞。取而代之的是,值的?*類型*?用來?*唯一*?控制值是通過值拷貝,還是引用拷貝來賦予。
讓我們來展示一下:
```source-js
var a = 2;
var b = a; // `b` 總是 `a` 中的值的拷貝
b++;
a; // 2
b; // 3
var c = [1,2,3];
var d = c; // `d` 是共享值 `[1,2,3]` 的引用
d.push( 4 );
c; // [1,2,3,4]
d; // [1,2,3,4]
```
簡單值(也叫基本標量)?*總是*?通過值拷貝來賦予/傳遞:`null`、`undefined`、`string`、`number`、?`boolean`、以及 ES6 的?`symbol`。
復合值 ——?`object`(包括?`array`,和所有的對象包裝器 —— 見第三章)和?`function`?——?*總是*?在賦值或傳遞時創建一個引用的拷貝。
在上面的代碼段中,因為?`2`?是一個基本標量,`a`?持有一個這個值的初始拷貝,而?`b`?被賦予了這個值的另一個拷貝。當改變?`b`?時,你根本沒有在改變?`a`?中的值。
但?`c`?和?`d`?兩個都?是同一個共享的值?`[1,2,3]`?的分離的引用。重要的是,`c`?和?`d`?對值?`[1,2,3]`?的“擁有”程度上是一樣的 —— 它們只是同一個值的對等引用。所以,不管使用哪一個引用去修改(`.push(4)`)實際上共享的?`array`?值本身,影響的僅僅是這一個共享值,而且這兩個引用將會指向新修改的值?`[1,2,3,4]`。
因為引用指向的是值本身而不是變量,你不能使用一個引用來改變另一個引用所指向的值:
```source-js
var a = [1,2,3];
var b = a;
a; // [1,2,3]
b; // [1,2,3]
// 稍后
b = [4,5,6];
a; // [1,2,3]
b; // [4,5,6]
```
當我們做賦值操作?`b = [4,5,6]`?時,我們做的事情絕對不會對?`a`?所指向的?*位置*(`[1,2,3]`)造成任何影響。如果那可能的話,`b`?就會是?`a`?的指針而不是這個?`array`?的引用 —— 但是這樣的能力在 JS 中是不存在的!
這樣的困惑最常見于函數參數:
```source-js
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 稍后
x = [4,5,6];
x.push( 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [1,2,3,4] 不是 [4,5,6,7]
```
當我們傳入參數?`a`?時,它將一份?`a`?引用的拷貝賦值給?`x`。`x`?和?`a`?是指向相同的?`[1,2,3]`?的不同引用。現在,在函數內部,我們可以使用這個引用來改變值本身(`push(4)`)。但是當我們進行賦值操作?`x = [4,5,6]`?時,不可能影響原來的引用?`a`?所指向的東西 —— 它仍然指向(已經被修改了的)值?`[1,2,3,4]`。
沒有辦法可以使用?`x`?引用來改變?`a`?指向哪里。我們只能修改?`a`?和?`x`?共通指向的那個共享值的內容。
要想改變?`a`?來使它擁有內容為?`[4,5,6,7]`?的值,你不能創建一個新的?`array`?并賦值 —— 你必須修改現存的?`array`?值:
```source-js
function foo(x) {
x.push( 4 );
x; // [1,2,3,4]
// 稍后
x.length = 0; // 原地清空既存的數組
x.push( 4, 5, 6, 7 );
x; // [4,5,6,7]
}
var a = [1,2,3];
foo( a );
a; // [4,5,6,7] 不是 [1,2,3,4]
```
正如你看到的,`x.length = 0`?和?`x.push(4,5,6,7)`?沒有創建一個新的?`array`,但是修改了現存的共享?`array`。所以理所當然地,`a`?引用了新的內容?`[4,5,6,7]`。
記住:你不能直接控制/覆蓋值拷貝和引用拷貝的行為 —— 這些語義是完全由當前值的類型來控制的。
為了實質上地通過值拷貝傳遞一個復合值(比如一個?`array`),你需要手動制造一個它的拷貝,使被傳遞的引用不指向原來的值。比如:
```source-js
foo( a.slice() );
```
不帶參數的?`slice(..)`?方法默認地為這個?`array`?制造一個全新的(淺)拷貝。所以,我們傳入的引用僅指向拷貝的?`array`,這樣?`foo(..)`?不會影響?`a`?的內容。
反之 —— 傳遞一個基本標量值,使它的值的變化可見,就像引用那樣 —— 你不得不將這個值包裝在另一個可以通過引用拷貝來傳遞的復合值中(`object`、`array`?等等):
```source-js
function foo(wrapper) {
wrapper.a = 42;
}
var obj = {
a: 2
};
foo( obj );
obj.a; // 42
```
這里,`obj`?作為基本標量屬性?`a`?的包裝。當傳遞給?`foo(..)`?時,一個?`obj`?引用的拷貝被傳入并設置給?`wrapper`?參數。我們現在可以使用?`wrapper`?引用來訪問這個共享的對象,并更新它的值。在函數完成時,`obj.a`?將被更新為值?`42`。
你可能會遇到這樣的情況,如果你想要傳入一個像?`2`?這樣的基本標量值的引用,你可以將這個值包裝在它的?`Number`?對象包裝器中(見第三章)。
這個?`Number`?對象的引用的拷貝?*將*?會被傳遞給函數是事實,但不幸的是,和你可能期望的不同,擁有一個共享獨享的引用不會給你修改這個共享的基本值的能力:
```source-js
function foo(x) {
x = x + 1;
x; // 3
}
var a = 2;
var b = new Number( a ); // 或等價的 `Object(a)`
foo( b );
console.log( b ); // 2, 不是 3
```
這里的問題是,底層的基本標量值是?*不可變的*(`String`?和?`Boolean`?也一樣)。如果一個?`Number`?對象持有一個基本標量值?`2`,那么這個?`Number`?對象就永遠不能再持有另一個值;你只能用一個不同的值創建一個全新的?`Number`?對象。
當?`x`?用于表達式?`x + 1`?時,底層的基本標量值?`2`?被自動地從?`Number`?對象中開箱(抽出),所以?`x = x + 1`?這一行很微妙地將?`x`?從一個共享的?`Number`?對象的引用,改變為僅持有加法操作?`2 + 1`?的結果?`3`?的基本標量值。因此,外面的?`b`?仍然引用原來的未被改變/不可變的,持有?`2`?的?`Number`?對象。
你?*可以*?在?`Number`?對象上添加屬性(只是不要改變它內部的基本值),所以你可間接地通過這些額外的屬性交換信息。
不過,這可不太常見;對大多數開發者來說這可能不是一個好的做法。
與其這樣使用?`Number`?包裝器對象,使用早先的代碼段中那樣的手動對象包裝器(`obj`)要好得多。這不是說像?`Number`?這樣包裝好的對象包裝器沒有用處 —— 而是說在大多數情況下,你可能應該優先使用基本標量值的形式。
引用十分強大,但是有時候它們礙你的事兒,而有時你會在它們不存在時需要它們。你唯一可以用來控制引用與值拷貝的東西是值本身的類型,所以你必須通過你選用的值的類型來間接地影響賦值/傳遞行為。
## 復習
在 JavaScript 中,`array`?僅僅是數字索引的集合,可以容納任何類型的值。`string`?是某種“類?`array`”,但它們有著不同的行為,如果你想要將它們作為?`array`?對待的話,必須要小心。JavaScript 中的數字既包括“整數”也包括浮點數。
幾種特殊值被定義在基本類型內部。
`null`?類型只有一個值?`null`,`undefined`?類型同樣地只有?`undefined`?值。對于任何沒有值存在的變量或屬性,`undefined`?基本上是默認值。`void`?操作符允許你從任意另一個值中創建?`undefined`?值。
`number`?包含幾種特殊值,比如?`NaN`(意為“不是一個數字”,但稱為“非法數字”更合適);`+Infinity`?和?`-Infinity`;還有?`-0`。
簡單基本標量(`string`、`number`?等)通過值拷貝進行賦值/傳遞,而復合值(`object`?等)通過引用拷貝進行賦值/傳遞。引用與其他語言中的引用/指針不同 —— 它們從不指向其他的變量/引用,而僅指向底層的值。