感謝社區(qū)中各位的大力支持,譯者再次奉上一點點福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠谣殊,并抽取幸運大獎:點擊這里領(lǐng)取
array
,string
牺弄,和number
是任何程序的最基礎(chǔ)構(gòu)建塊姻几,但是JavaScript在這些類型上有一些要么使你驚喜要么使你驚訝的獨特性質(zhì)。
讓我們來看幾種JS內(nèi)建的值類型势告,并探討一下我們?nèi)绾尾拍芨尤娴乩斫獠⒄_地利用它們的行為蛇捌。
Array
和其他強(qiáng)制類型的語言相比,JavaScript的array
只是值的容器咱台,而這些值可以是任何類型:string
或者number
或者object
络拌,甚至是另一個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
警告: 在一個array
值上使用delete
將會從這個array
上移除一個值槽春贸,但就算你移除了最后一個元素混萝,它也 不會 更新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
鍵/屬性(但是這些屬性不會計算在array
的length
中):
var a = [ ];
a[0] = 1;
a["foobar"] = 2;
a.length; // 1
a["foobar"]; // 2
a.foobar; // 2
然而阔挠,一個需要小心的坑是飘庄,如果一個可以被強(qiáng)制轉(zhuǎn)換為10進(jìn)制number
的string
值被用作鍵的話,它會認(rèn)為你想使用number
索引而不是一個string
鍵购撼!
var a = [ ];
a["13"] = 42;
a.length; // 14
一般來說跪削,向array
添加string
鍵/屬性不是一個好主意。最好使用object
來持有鍵/屬性形式的值迂求,而將array
專用于嚴(yán)格地數(shù)字索引的值碾盐。
類Array
偶爾你需要將一個類array
值(一個數(shù)字索引的值的集合)轉(zhuǎn)換為一個真正的array
,通常你可以對這些值的集合調(diào)用數(shù)組的工具函數(shù)(比如indexOf(..)
揩局,concat(..)
毫玖,forEach(..)
等等)。
舉個例子凌盯,各種DOM查詢操作會返回一個DOM元素的列表付枫,對于我們轉(zhuǎn)換的目的來說,這些列表不是真正的array
但是也足夠類似array
驰怎。另一個常見的例子是阐滩,函數(shù)為了像列表一樣訪問它的參數(shù)值,而暴露了arugumens
對象(類array
县忌,在ES6中被廢棄了)叶眉。
一個進(jìn)行這種轉(zhuǎn)換的很常見的方法是對這個值借用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ù)制這個array
(或者,在這個例子中莲趣,是一個類array
)的效果鸳慈。
在ES6中,還有一種稱為Array.from(..)
的內(nèi)建工具可以執(zhí)行相同的任務(wù):
...
var arr = Array.from( arguments );
...
注意: Array.from(..)
擁有幾種其他強(qiáng)大的能力喧伞,我們將在本系列的 ES6與未來 中涵蓋它的細(xì)節(jié)走芋。
String
一個很常見的想法是绩郎,string
實質(zhì)上只是字符的array
。雖然內(nèi)部的實現(xiàn)可能是也可能不是array
翁逞,但重要的是要理解JavaScript的string
與字符的array
確實不一樣肋杖。它們的相似性幾乎只是表面上的。
舉個例子挖函,讓我們考慮這兩個值:
var a = "foo";
var b = ["f","o","o"];
String確實與array
有很膚淺的相似性 -- 也就是上面說的状植,類array
-- 舉例來說,它們都有一個length
屬性怨喘,一個indexOf(..)
方法(在ES5中僅有array
版本)津畸,和一個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
上沒有一個方法是可以原地修改它的內(nèi)容的芋浮,而是創(chuàng)建并返回一個新的string
。與之相對的是壳快,許多改變array
內(nèi)容的方法實際上 是 原地修改的纸巷。
c = a.toUpperCase();
a === c; // false
a; // "foo"
c; // "FOO"
b.push( "!" );
b; // ["f","O","o","!"]
另外,許多array
方法在處理string
時非常有用眶痰,雖然這些方法不屬于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."
讓我們來看另一個例子:翻轉(zhuǎn)一個string
(順帶一提,這是一個JavaScript面試中常見的細(xì)小問題J)存哲。array
擁有一個原地的reverse()
修改器方法,但是string
沒有:
a.reverse; // undefined
b.reverse(); // ["!","o","O","f"]
b; // ["!","o","O","f"]
不幸的是七婴,這種“借用”array
修改器不起作用祟偷,因為string
是不可變的,因此它不能被原地修改:
Array.prototype.reverse.call( a );
// 仍然返回一個“foo”的String對象包裝器(見第三章) :(
另一種迂回的做法(也是黑科技)是打厘,將string
轉(zhuǎn)換為一個array
修肠,實施我們想做的操作,然后將它轉(zhuǎn)回string
户盯。
var c = a
// 將`a`切分成一個字符的數(shù)組
.split( "" )
// 翻轉(zhuǎn)字符的數(shù)組
.reverse()
// 將字符的數(shù)組連接回一個字符串
.join( "" );
c; // "oof"
如果你覺得這很難看嵌施,沒錯饲化。不管怎樣,對于簡單的string
它 好用吗伤,所以如果你需要某些快速但是“臟”的東西吃靠,像這樣的方式經(jīng)常能滿足你。
警告: 小心足淆!這種方法對含有復(fù)雜(unicode)字符(星號巢块,多字節(jié)字符等)的string
不起作用。你需要支持unicode的更精巧的工具庫來準(zhǔn)確地處理這種操作缸浦。在這個問題上可以咨詢Mathias Bynens的作品:Esrever(https://github.com/mathiasbynens/esrever)夕冲。
另外一種考慮這個問題的方式是:如果你更經(jīng)常地將你的“string”基本上作為 字符的數(shù)組 來執(zhí)行一些任務(wù)的話,也許就將它們作為array
而不是作為string
存儲更好裂逐。你可能會因此省去很多每次都將string
轉(zhuǎn)換為array
的麻煩歹鱼。無論何時你確實需要string
的表現(xiàn)形式的話,你總是可以調(diào)用 字符的 array
的join("")
方法卜高。
Number
JavaScript只有一種數(shù)字類型:number
弥姻。這種類型包含“整數(shù)”值和小數(shù)值。我說“整數(shù)”時加了引號掺涛,因為JS的一個長久以來為人詬病的原因是庭敦,和其他語言不同,JS沒有真正的整數(shù)薪缆。這可能在未來某個時候會改變秧廉,但是目前,我們只有number
可用拣帽。
所以疼电,在JS中,一個“整數(shù)”只是一個沒有小數(shù)部分的小數(shù)值减拭。也就是說蔽豺,42.0
和42
一樣是“整數(shù)”。
像大多數(shù)現(xiàn)代計算機(jī)語言拧粪,以及幾乎所有的腳本語言一樣修陡,JavaScript的number
的實現(xiàn)基于“IEEE 754”標(biāo)準(zhǔn),通常被稱為“浮點”可霎。JavaScript明確地使用了這個標(biāo)準(zhǔn)的“雙精度”(也就是“64位二進(jìn)制”)格式魄鸦。
在網(wǎng)絡(luò)上有許多了不起的文章都在介紹二進(jìn)制浮點數(shù)如何在內(nèi)存中存儲的細(xì)節(jié),以及選擇這些做法的意義癣朗。因為對于理解如何在JS中正確使用number
來說号杏,理解內(nèi)存中的位模式不是必須的,所以我們將這個話題作為練習(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;
相似地荣暮,一個小數(shù)在.
之后的小數(shù)部分如果是0
庭惜,是可選的:
var a = 42.0;
var b = 42.;
警告: 42.
是極不常見的,如果你正在努力避免別人閱讀你的代碼時感到困惑穗酥,它可能不是一個好主意护赊。但不管怎樣,它是合法的砾跃。
默認(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
因為number
值可以用Number
對象包裝器封裝(見第三章)翘骂,number
值可以訪問內(nèi)建在Number.prototype
上的方法(見第三章)壁熄。舉個例子,toFixed(..)
方法允許你指定一個值在被表示時碳竟,帶有多少位小數(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"
要注意的是草丧,它的輸出實際上是一個number
的string
表現(xiàn)形式,而且如果你指定的位數(shù)多于值持有的小數(shù)位數(shù)時莹桅,會在右側(cè)補0
昌执。
toPrecision(..)
很相似,但它指定的是有多少 有效數(shù)字 用來表示這個值:
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
的字面上訪問這些方法懂拾。但你不得不小心.
操作符。因為.
是一個合法數(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)
是不合法的語法贾节,因為.
作為42.
字面(這是合法的 -- 參見上面的討論!)的一部分被吞掉了衷畦,因此沒有.
屬性操作符來表示.toFixed
訪問栗涂。
42..toFixed(3)
可以工作,因為第一個.
是number
的一部分祈争,而第二個.
是屬性操作符斤程。但它可能看起來很古怪,而且確實在實際的JavaScript代碼中很少會看到這樣的東西。實際上忿墅,在任何基本類型上直接訪問方法是十分不常見的扁藕。但是不常見并不意味著 壞 或者 錯。
注意: 有一些庫擴(kuò)展了內(nèi)建的Number.prototype
(見第三章)疚脐,使用number
或在number
上提供了額外的操作亿柑,所以在這些情況下,像使用10..makeItRain()
來設(shè)定一個10秒鐘的下錢雨的動畫棍弄,或者其他諸如此類的傻事是完全合法的望薄。
在技術(shù)上講,這也是合法的(注意那個空格):
42 .toFixed(3); // "42.000"
但是呼畸,尤其是對number
字面量來說痕支,這是特別使人糊涂的代碼風(fēng)格,而且除了使其他開發(fā)者(和未來的你)糊涂以外沒有任何用處蛮原。避免它卧须。
number
還可以使用科學(xué)計數(shù)法的形式指定,這在表示很大的number
時很常見瞬痘,比如:
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)停止使用它,來擁抱未來(而且因為你現(xiàn)在應(yīng)當(dāng)在使用strict
模式了4亍)闸度。
至于ES6,下面的新形式也是合法的:
0o363; // 八進(jìn)制的: 243
0O363; // 同上
0b11110011; // 二進(jìn)制的: 243
0B11110011; // 同上
請為你的開發(fā)者同胞們做件好事:絕不要使用0O363
形式蚜印。把0
放在大寫的O
旁邊就是在制造困惑莺禁。保持使用小寫的謂詞0x
,0b
窄赋,和0o
哟冬。
小數(shù)值
使用二進(jìn)制浮點數(shù)的最出名(臭名昭著)的副作用是(記住,這是對 所有 使用IEEE 754的語言都成立的——不是許多人認(rèn)為/假裝 僅 在JavaScript中存在的問題):
0.1 + 0.2 === 0.3; // false
從數(shù)學(xué)的意義上忆绰,我們知道這個語句應(yīng)當(dāng)為true
浩峡。為什么它是false
?
簡單地說错敢,0.1
和0.2
的二進(jìn)制表示形式是不精確的翰灾,所以它們相加時,結(jié)果不是精確地0.3
。而是 非常 接近的值:0.30000000000000004
纸淮,但是如果你的比較失敗了平斩,“接近”是無關(guān)緊要的。
注意: JavaScript應(yīng)當(dāng)切換到可以精確表達(dá)所有值的一個不同的number
實現(xiàn)嗎萎馅?有些人認(rèn)為應(yīng)該双戳。多年以來有許多選項出現(xiàn)過。但是沒有一個被采納糜芳,而且也許永遠(yuǎn)也不會。它看起來就像揮揮手然后說“已經(jīng)改好那個bug了!”那么簡單魄衅,但根本不是那么回事兒峭竣。如果真有這么簡單,它絕對就在很久以前被改掉了晃虫。
現(xiàn)在的問題是皆撩,如果一些number
不能被 信任 為精確的,這不是意味著我們根本不能使用number
嗎哲银? 當(dāng)然不是扛吞。
在一些應(yīng)用程序中你需要多加小心,特別是在對付小數(shù)的時候荆责。還有許多(也許是大多數(shù)滥比?)應(yīng)用程序只處理整數(shù),而且做院,最大只處理到幾百萬到幾萬億盲泛。這些應(yīng)用程序使用JS中的數(shù)字操作是,而且將總是键耕,非常安全 的寺滚。
要是我們 確實 需要比較兩個number
,就像0.1 + 0.2
與0.3
屈雄,而且知道這個簡單的相等測試將會失敗呢村视?
可以接受的最常見的做法是使用一個很小的“錯誤舍入”值作為比較的 容差。這個很小的值經(jīng)常被稱為“機(jī)械極小值(machine epsilon)”酒奶,對于JavaScript來說這種number
通常為2^-52
(2.220446049250313e-16
)蚁孔。
在ES6中,使用這個容差值預(yù)定義了Number.EPSILON
讥蟆,所以你將會想要使用它勒虾,你也可以在前ES6中安全地填補這個定義:
if (!Number.EPSILON) {
Number.EPSILON = Math.pow(2,-52);
}
我們可以使用這個Number.EPSILON
來比較兩個number
的“等價性”(帶有錯誤舍入的容差):
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
(它真的非常,非常瘸彤,非常大P奕弧),它為你預(yù)定義為Number.MAX_VALUE
。在極小的一端愕宋,Number.MIN_VALUE
大概是5e-324
玻靡,它不是負(fù)數(shù)但是非常接近于0!
安全整數(shù)范圍
由于number
的表示方式中贝,對完全是number
的“整數(shù)”而言有一個“安全”的值的范圍囤捻,而且它要比Number.MAX_VALUE
小得多。
可以“安全地”被表示的最大整數(shù)(也就是說邻寿,可以保證被表示的值是實際可以無誤地表示的)是2^53 - 1
蝎土,也就是9007199254740991
,如果你插入一些數(shù)字分隔符绣否,可以看到它剛好超過9萬億誊涯。所以對于number
能表示的上限來說它確實是夠TM大的。
在ES6中這個值實際上是自動預(yù)定義的蒜撮,它是Number.MAX_SAFE_INTEGER
暴构。意料之中的是,還有一個最小值段磨,-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ù)學(xué)操作邑时,目前來講你需要使用一個 大數(shù)字 工具。在未來版本的JavaScript中特姐,大數(shù)字也許會得到官方支持晶丘。
測試整數(shù)
測試一個值是否是整數(shù),你可以使用ES6定義的Number.isInteger(..)
:
Number.isInteger( 42 ); // true
Number.isInteger( 42.000 ); // true
Number.isInteger( 42.3 ); // false
可以為前ES6填補Number.isInteger(..)
:
if (!Number.isInteger) {
Number.isInteger = function(num) {
return typeof num == "number" && num % 1 == 0;
};
}
要測試一個值是否是 安全整數(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瀏覽器填補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
來說捷枯,“安全范圍”一定會小得多滚秩。
這個范圍是從Math.pow(-2,31)
(-2147483648
,大約-21億)到Math.pow(2,31)-1
(2147483647
淮捆,大約+21億)郁油。
要強(qiáng)制a
中的number
值是32位有符號整數(shù)本股,使用a | 0
。這可以工作是因為|
位操作符僅僅對32位值起作用(意味著它可以只關(guān)注32位桐腌,而其他的位將被丟掉)拄显。而且,和0進(jìn)行“或”的位操作實質(zhì)上是什么也不做案站。
注意: 特定的特殊值(我們將在下一節(jié)討論)躬审,比如NaN
和Infinity
不是“32位安全”的,當(dāng)這些值被傳入位操作符時將會通過一個抽象操作ToInt32
(見第四章)并為了位操作而簡單地變成+0
值蟆盐。
特殊值
在各種類型中散布著一些特殊值承边,需要 警惕 的JS開發(fā)者小心,并正確使用石挂。
不是值的值
對于undefined
類型來說炒刁,有且僅有一個值:undefined
。對于null
類型來說誊稚,有且僅有一個值:null
。所以對它們而言罗心,這些文字既是它們的類型也是它們的值里伯。
undefined
和null
作為“空”值或者“沒有”值,經(jīng)常被認(rèn)為是可以互換的渤闷。另一些開發(fā)者偏好于使用微妙的區(qū)別將它們區(qū)分開。舉例來講:
-
null
是一個空值 -
undefined
是一個丟失的值
或者:
-
undefined
還沒有值 -
null
曾經(jīng)有過值但現(xiàn)在沒有
不管你選擇如何“定義”和使用這兩個值,null
是一個特殊的關(guān)鍵字易核,不是一個標(biāo)識符遏弱,因此你不能將它作為一個變量對待來給它賦值(為什么你要給它賦值呢?O阴濉)肩碟。然而,undefined
(不幸地)是 一個標(biāo)識符凸椿。噢削祈。
Undefined
在非strict
模式下,給在全局上提供的undefined
標(biāo)識符賦一個值實際上是可能的(雖然這是一個非常不好的做法D月):
function foo() {
undefined = 2; // 非常差勁兒的主意髓抑!
}
foo();
function foo() {
"use strict";
undefined = 2; // TypeError!
}
foo();
但是,在非strict
模式和strict
模式下优幸,你可以創(chuàng)建一個名叫undefined
局部變量吨拍。但這又是一個很差勁兒的主意!
function foo() {
"use strict";
var undefined = 2;
console.log( undefined ); // 2
}
foo();
朋友永遠(yuǎn)不讓朋友覆蓋undefined
网杆。
void
操作符
雖然undefined
是一個持有內(nèi)建的值undefined
的內(nèi)建標(biāo)識符(除非被修改——見上面的討論8巍)伊滋,另一個得到這個值的方法是void
操作符。
表達(dá)式void __
會“躲開”任何值严里,所以這個表達(dá)式的結(jié)果總是值undefined
新啼。它不會修改任何已經(jīng)存在的值;只是確保不會有值從操作符表達(dá)式中返回來刹碾。
var a = 42;
console.log( void a, a ); // undefined 42
從慣例上講(大約是從C語言編程中發(fā)展而來)燥撞,要通過使用void
來獨立表現(xiàn)值undefined
,你可以使用void 0
(雖然迷帜,很明顯物舒,void true
或者任何其他的void
表達(dá)式都做同樣的事情)。在void 0
戏锹,void 1
和undefined
之間沒有實際上的區(qū)別冠胯。
但是在幾種其他的環(huán)境下void
操作符可以十分有用:如果你需要確保一個表達(dá)式?jīng)]有結(jié)果值(即便它有副作用)。
舉個例子:
function doSomething() {
// 注意:`APP.ready`是由我們的應(yīng)用程序提供的
if (!APP.ready) {
// 稍后再試一次
return void setTimeout( doSomething, 100 );
}
var result;
// 做其他一些事情
return result;
}
// 我們能立即執(zhí)行嗎锦针?
if (doSomething()) {
// 馬上處理其他任務(wù)
}
這里荠察,setTimeout(..)
函數(shù)返回一個數(shù)字值(時間間隔定時器的唯一標(biāo)識符,用于取消它自己)奈搜,但是我們想void
它悉盆,這樣我們函數(shù)的返回值不會在if
語句上給出一個成立的誤報。
許多開發(fā)者寧愿將這些動作分開馋吗,這樣的效用相同但不使用void
操作符:
if (!APP.ready) {
// 稍后再試一次
setTimeout( doSomething, 100 );
return;
}
一般來說焕盟,如果有那么一個地方,有一個值存在(來自某個表達(dá)式)而你發(fā)現(xiàn)這個值如果是undefined
才有用宏粤,就使用void
操作符脚翘。這可能在你的程序中不是非常常見,但如果在一些稀有的情況下你需要它绍哎,它就十分有用来农。
特殊的數(shù)字
number
類型包含幾種特殊值。我們將會仔細(xì)考察每一種蛇摸。
不是數(shù)字的數(shù)字
如果你不使用同為number
(或者可以被翻譯為10進(jìn)制或16進(jìn)制的普通number
的值)的兩個操作數(shù)進(jìn)行任何算數(shù)操作备图,那么操作的結(jié)果將失敗而產(chǎn)生一個不合法的number
,在這種情況下你將得到NaN
值赶袄。
NaN
在字面上代表“不是一個number
(Not a Number)”揽涮,但是正如我們即將看到的,這種文字描述十分失敗而且容易誤導(dǎo)人饿肺。將NaN
考慮為“不合法數(shù)字”蒋困,“失敗的數(shù)字”,甚至是“壞掉的數(shù)字”都要比“不是一個數(shù)字”準(zhǔn)確得多敬辣。
舉例來說:
var a = 2 / "foo"; // NaN
typeof a === "number"; // true
換句話說:“‘不是一個數(shù)字’的類型是‘?dāng)?shù)字’”雪标!為這使人糊涂的名字和語義歡呼吧零院。
NaN
是一種“哨兵值”(一個被賦予了特殊意義的普通的值),它代表number
集合內(nèi)的一種特殊的錯誤情況村刨。這種錯誤情況實質(zhì)上是:“我試著進(jìn)行數(shù)學(xué)操作但是失敗了告抄,而這就是失敗的number
結(jié)果∏段”
那么打洼,如果你有一個值存在某個變量中,而且你想要測試它是否是這個特殊的失敗數(shù)字NaN
逆粹,你也許認(rèn)為你可以直接將它與NaN
本身比較募疮,就像你能對其它的值做的那樣,比如null
和undefined
僻弹。不是這樣阿浓。
var a = 2 / "foo";
a == NaN; // false
a === NaN; // false
NaN
是一個非常特殊的值,它從來不會等于另一個NaN
值(也就是蹋绽,它從來不等于它自己)芭毙。實際上,它是唯一一個不具有反射性的值(沒有恒等性x === x
)卸耘。所以稿蹲,NaN !== NaN
。有點奇怪鹊奖,對吧?
那么涂炎,如果不能與NaN
進(jìn)行比較(因為這種比較將總是失斨揖邸),我們該如何測試它呢唱捣?
var a = 2 / "foo";
isNaN( a ); // true
夠簡單的吧两蟀?我們使用稱為isNaN(..)
的內(nèi)建全局工具,它告訴我們這個值是否是NaN
震缭。問題解決了赂毯!
別高興得太早。
isNaN(..)
工具有一個重大缺陷拣宰。它似乎過于按照字面的意思(“不是一個數(shù)字”)去理解NaN
的含義了——它的工作基本上是:“測試這個傳進(jìn)來的東西是否不是一個number
或者是一個number
”党涕。但這不是十分準(zhǔn)確。
var a = 2 / "foo";
var b = "foo";
a; // NaN
b; // "foo"
window.isNaN( a ); // true
window.isNaN( b ); // true -- 噢!
很明顯巡社,"foo"
根本 不是一個number
膛堤,但它也絕不是一個NaN
值!這個bug從最開始的時候就存在于JS中了(存在超過19年的坑)晌该。
在ES6中肥荔,終于提供了一個替代它的工具:Number.isNaN(..)
绿渣。有一個簡單的填補,可以讓你即使是在前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 -- 咻!
實際上燕耿,通過利用NaN
與它自己不相等這個特殊的事實中符,我們可以更簡單地實現(xiàn)Number.isNaN(..)
的填補。在整個語言中NaN
是唯一一個這樣的值誉帅;其他的值都總是 等于它自己淀散。
所以:
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
怪吧?但是好用堵第!
不管有意還是無意吧凉,在許多真實世界的JS程序中NaN
可能是一個現(xiàn)實的問題。使用Number.isNaN(..)
(或者它的填補)這樣的可靠測試來正確地識別它們是一個非常好的主意踏志。
如果你正在程序中僅使用isNaN(..)
阀捅,悲慘的現(xiàn)實是你的程序 有bug,即便是你還沒有被它咬到针余!
無窮
來自于像C這樣的傳統(tǒng)編譯型語言的開發(fā)者饲鄙,可能習(xí)慣于看到編譯器錯誤或者是運行時異常,比如對這樣一個操作給出的“除數(shù)為0”:
var a = 1 / 0;
然而在JS中圆雁,這個操作是明確定義的忍级,而且它的結(jié)果是值Infinity
(也就是Number.POSITIVE_INFINITY
)。意料之中的是:
var a = 1 / 0; // Infinity
var b = -1 / 0; // -Infinity
如你所見伪朽,-Infinity
(也就是Number.NEGATIVE_INFINITY
)是從任一個被除數(shù)為負(fù)(不是兩個都是負(fù)數(shù)V嵩邸)的除0操作得來的。
JS使用有限的數(shù)字表現(xiàn)形式(IEEE 754 浮點烈涮,我們早先討論過)朴肺,所以和單純的數(shù)學(xué)相比,它看起來甚至在做加法和減法這樣的操作時都有可能溢出坚洽,這樣的情況下你將會得到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ī)范,如果一個像加法這樣的操作得到一個太大而不能表示的值讶舰,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)真的历帚,停滔岳!
一旦你溢出了任意一個 無限值,那么挽牢,就沒有回頭路了谱煤。換句最有詩意的話說,你可以從有限邁向無限禽拔,但不能從無限回歸有限刘离。
“無限除以無限等于什么”,這簡直是一個哲學(xué)問題睹栖。我們幼稚的大腦可能會說“1”或“無限”硫惕。事實表明它們都不對。在數(shù)學(xué)上和在JavaScript中野来,Infinity / Infinity
不是一個有定義的操作恼除。在JS中,它的結(jié)果為NaN
曼氛。
一個有限的正number
除以Infinity
呢豁辉?簡單!0
舀患。那一個有限的負(fù)number
處理Infinity
呢徽级?接著往下讀!
零
雖然這可能使有數(shù)學(xué)頭腦的讀者困惑聊浅,JavaScript擁有普通的零0
(也稱為正零+0
) 和 一個負(fù)零-0
餐抢。在我們講解為什么-0
存在之前,我們應(yīng)該考察JS如何處理它低匙,因為它可能十分令人困惑弹澎。
除了使用字面量-0
指定,負(fù)的零還可以從特定的數(shù)學(xué)操作中得出努咐。比如:
var a = 0 / -3; // -0
var b = 0 * -3; // -0
加法和減法無法得出負(fù)零。
在開發(fā)者控制臺中考察一個負(fù)的零殴胧,經(jīng)常顯示為-0
渗稍,然而直到最近這才是一個常見情況,所以一些你可能遇到的老版本瀏覽器也許依然將它報告為0
团滥。
但是根據(jù)語言規(guī)范竿屹,如果你試著將一個負(fù)零轉(zhuǎn)換為字符串,它將總會被報告為"0"
灸姊。
var a = 0 / -3;
// 至少(有些瀏覽器)控制臺是對的
a; // -0
// 但是語言規(guī)范堅持要向你撒謊拱燃!
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)你觀察的時候碗誉,JSON.stringify( -0 )
產(chǎn)生"0"
顯得特別奇怪召嘶,因為它與反向操作不符:JSON.parse( "-0" )
將像你期望地那樣報告-0
。
除了一個負(fù)零的字符串化會欺騙性地隱藏它實際的值外哮缺,比較操作符也被設(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)槭裁葱枰粋€負(fù)零呢淳玩?
在一些應(yīng)用程序中,開發(fā)者使用值的大小來表示一部分信息(比如動畫中每一幀的速度)非竿,而這個number
的符號來表示另一部分信息(比如移動的方向)蜕着。
在這些應(yīng)用程序中,舉例來說汽馋,如果一個變量的值變成了0侮东,而它丟失了符號,那么你就丟失了它是從哪個方向移動到0的信息豹芯。保留零的符號避免了潛在的意外信息丟失悄雅。
特殊等價
正如我們上面看到的,當(dāng)使用等價性比較時铁蹈,值NaN
和值-0
擁有特殊的行為宽闲。NaN
永遠(yuǎn)不會和自己相等,所以你不得不使用ES6的Number.isNaN(..)
(或者它的填補)握牧。相似地容诬,-0
撒謊并假裝它和普通的正零相等(即使使用===
嚴(yán)格等價——見第四章),所以你不得不使用我們上面建議的某些isNegZero(..)
黑科技工具沿腰。
在ES6中览徒,有一個新工具可以用于測試兩個值的絕對等價性,而沒有任何這些例外颂龙。它稱為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)境习蓬,這是一個相當(dāng)簡單的Object.is(..)
填補:
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)換”),因為這些操作符可能高效得多措嵌,并且更慣用/常見躲叼。Object.is(..)
很大程度上是為這些特殊的等價情況準(zhǔn)備的。
值與引用
在其他許多語言中企巢,根據(jù)你使用的語法枫慷,值可以通過值拷貝,也可以通過引用拷貝來賦予/傳遞。
比如或听,在C++中如果你想要把一個number
變量傳遞進(jìn)一個函數(shù)探孝,并使這個變量的值被更新,你可以用int& myNum
這樣的東西來聲明函數(shù)參數(shù)神帅,當(dāng)你傳入一個變量x
時再姑,myNum
將是一個 指向x
的引用;引用就像一個特殊形式的指針找御,你得到的是一個指向另一個變量的指針(像一個 別名(alias)) 元镀。如果你沒有聲明一個引用參數(shù),被傳入的值將 總是 被拷貝的霎桅,就算它是一個復(fù)雜的對象栖疑。
在JavaScript中,沒有指針滔驶,并且引用的工作方式有一點兒不同遇革。你不能擁有一個從一個JS變量到另一個JS變量的引用。這是完全不可能的揭糕。
JS中的引用指向一個(共享的) 值萝快,所以如果你有10個不同的引用,它們都總是同一個共享值的不同引用著角;它們沒有一個是另一個的引用/指針揪漩。
另外,在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
——總是 在賦值或傳遞時創(chuàng)建一個引用的拷貝痴荐。
在上面的代碼段中,因為2
是一個基本標(biāo)量官册,a
持有一個這個值的初始拷貝生兆,而b
被賦予了這個值的另一個拷貝。當(dāng)改變b
時,你根本沒有在改變a
中的值鸦难。
但 c
和d
兩個都 是同一個共享的值[1,2,3]
的分離的引用根吁。重要的是,c
和d
對值[1,2,3]
的“擁有”程度上是一樣的——它們只是同一個值的對等引用合蔽。所以击敌,不管使用哪一個引用去修改(.push(4)
)實際上共享的array
值本身,影響的僅僅是這一個共享值拴事,而且這兩個引用將會指向新修改的值[1,2,3,4]
沃斤。
因為引用指向的是值本身而不是變量,你不能使用一個引用來改變另一個引用所指向的值:
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]
時刃宵,我們做的事情絕對不會對a
所指向的 位置([1,2,3]
)造成任何影響衡瓶。如果那可能的話,b
就會是a
的指針而不是這個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
時哮针,它將一份a
引用的拷貝賦值給x
。x
和a
是指向相同的[1,2,3]
的不同引用√古郏現(xiàn)在十厢,在函數(shù)內(nèi)部,我們可以使用這個引用來改變值本身(push(4)
)捂齐。但是當(dāng)我們進(jìn)行賦值操作x = [4,5,6]
時蛮放,不可能影響原來的引用a
所指向的東西——它仍然指向(已經(jīng)被修改了的)值[1,2,3,4]
。
沒有辦法可以使用x
引用來改變a
指向哪里辛燥。我們只能修改a
和x
共通指向的那個共享值的內(nèi)容筛武。
要想改變a
來使它擁有內(nèi)容為[4,5,6,7]
的值,你不能創(chuàng)建一個新的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)建一個新的array
徘六,但是修改了現(xiàn)存的共享array
。所以理所當(dāng)然地榴都,a
引用了新的內(nèi)容[4,5,6,7]
待锈。
記住:你不能直接控制/覆蓋值拷貝和引用拷貝的行為 —— 這些語義是完全由當(dāng)前值的類型來控制的嘴高。
為了實質(zhì)上地通過值拷貝傳遞一個復(fù)合值(比如一個array
)竿音,你需要手動制造一個它的拷貝,使被傳遞的引用不指向原來的值拴驮。比如:
foo( a.slice() );
不帶參數(shù)的slice(..)
方法默認(rèn)地為這個array
制造一個全新的(淺)拷貝春瞬。所以,我們傳入的引用僅指向拷貝的array
套啤,這樣foo(..)
不會影響a
的內(nèi)容宽气。
反之 —— 傳遞一個基本標(biāo)量值,使它的值的變化可見,就像引用那樣 —— 你不得不將這個值包裝在另一個可以通過引用拷貝來傳遞的復(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(..)
時枣察,一個obj
引用的拷貝被傳入并設(shè)置給wrapper
參數(shù)。我們現(xiàn)在可以使用wrapper
引用來訪問這個共享的對象燃逻,并更新它的值序目。在函數(shù)完成時,obj.a
將被更新為值42
唆樊。
你可能會遇到這樣的情況宛琅,如果你想要傳入一個像2
這樣的基本標(biāo)量值的引用,你可以將這個值包裝在它的Number
對象包裝器中(見第三章)逗旁。
這個Number
對象的引用的拷貝 將 會被傳遞給函數(shù)是事實嘿辟,但不幸的是,和你可能期望的不同片效,擁有一個共享獨享的引用不會給你修改這個共享的基本值的能力:
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
這里的問題是红伦,底層的基本標(biāo)量值是 不可變的(String
和Boolean
也一樣)。如果一個Number
對象持有一個基本標(biāo)量值2
淀衣,那么這個Number
對象就永遠(yuǎn)不能再持有另一個值昙读;你只能用一個不同的值創(chuàng)建一個全新的Number
對象。
當(dāng)x
用于表達(dá)式x + 1
時膨桥,底層的基本標(biāo)量值2
被自動地從Number
對象中開箱(抽出)蛮浑,所以x = x + 1
這一行很微妙地將x
從一個共享的Number
對象的引用,改變?yōu)閮H持有加法操作2 + 1
的結(jié)果3
的基本標(biāo)量值只嚣。因此沮稚,外面的b
仍然引用原來的未被改變/不可變的,持有2
的Number
對象册舞。
你 可以 在Number
對象上添加屬性(只是不要改變它內(nèi)部的基本值)蕴掏,所以你可間接地通過這些額外的屬性交換信息。
不過调鲸,這可不太常見盛杰;對大多數(shù)開發(fā)者來說這可能不是一個好的做法。
與其這樣使用Number
包裝器對象藐石,使用早先的代碼段中那樣的手動對象包裝器(obj
)要好得多即供。這不是說像Number
這樣包裝好的對象包裝器沒有用處——而是說在大多數(shù)情況下,你可能應(yīng)該優(yōu)先使用基本標(biāo)量值的形式于微。
引用十分強(qiáng)大逗嫡,但是有時候它們礙你的事兒办素,而有時你會在它們不存在時需要它們。你唯一可以用來控制引用與值拷貝的東西是值本身的類型祸穷,所以你必須通過你選用的值的類型來間接地影響賦值/傳遞行為。
復(fù)習(xí)
在JavaScript中勺三,array
僅僅是數(shù)字索引的集合雷滚,可以容納任何類型的值。string
是某種“類array
”吗坚,但它們有著不同的行為祈远,如果你想要將它們作為array
對待的話,必須要小心商源。JavaScript中的數(shù)字既包括“整數(shù)”也包括浮點數(shù)车份。
幾種特殊值被定義在基本類型內(nèi)部牡彻。
null
類型只有一個值null
,undefined
類型同樣地只有undefined
值缎除。對于任何沒有值存在的變量或?qū)傩裕?code>undefined基本上是默認(rèn)值。void
操作符允許你從任意另一個值中創(chuàng)建undefined
值总寻。
number
包含幾種特殊值渐行,比如NaN
(意為“不是一個數(shù)字”轰坊,但稱為“非法數(shù)字”更合適);+Infinity
和-Infinity
祟印; 還有-0
旁理。
簡單基本標(biāo)量(string
,number
等)通過值拷貝進(jìn)行賦值/傳遞驻襟,而復(fù)合值(object
等)通過引用拷貝進(jìn)行賦值/傳遞芋哭。引用與其他語言中的引用/指針不同 —— 它們從不指向其他的變量/引用减牺,而僅指向底層的值。