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