第四章:強(qiáng)制轉(zhuǎn)換1

特別說明潜索,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS

現(xiàn)在我們更全面地了解了JavaScript的類型和值翰苫,我們將注意力轉(zhuǎn)向一個(gè)極具爭(zhēng)議的話題:強(qiáng)制轉(zhuǎn)換征唬。

正如我們?cè)诘谝徽轮刑岬降模P(guān)于強(qiáng)制轉(zhuǎn)換到底是一個(gè)有用的特性俱箱,還是一個(gè)語言設(shè)計(jì)上的缺陷(或位于兩者之間!)灭必,早就開始就爭(zhēng)論不休了狞谱。如果你讀過關(guān)于JS的其他書籍,你就會(huì)知道流行在世面上那種淹沒一切的 聲音:強(qiáng)制轉(zhuǎn)換是魔法禁漓,是邪惡的跟衅,令人困惑的,而且就是徹頭徹尾的壞主意播歼。

本著這個(gè)系列叢書的總體精神伶跷,我認(rèn)為你應(yīng)當(dāng)直面你不理解的東西并設(shè)法更全面地 搞懂它。而不是因?yàn)榇蠹叶歼@樣做秘狞,或是你曾經(jīng)被一些怪東西咬到就逃避強(qiáng)制轉(zhuǎn)換叭莫。

我們的目標(biāo)是全面地探索強(qiáng)制轉(zhuǎn)換的優(yōu)點(diǎn)和缺點(diǎn)(是的,它們 優(yōu)點(diǎn)K甘浴)雇初,這樣你就能在程序中對(duì)它是否合適做出明智的決定。

轉(zhuǎn)換值

將一個(gè)值從一個(gè)類型明確地轉(zhuǎn)換到另一個(gè)類型通常稱為“類型轉(zhuǎn)換(type casting)”减响,當(dāng)這個(gè)操作隱含地完成時(shí)稱為“強(qiáng)制轉(zhuǎn)換(coercion)”(根據(jù)一個(gè)值如何被使用的規(guī)則來強(qiáng)制它變換類型)靖诗。

注意: 這可能不明顯,但是JavaScript強(qiáng)制轉(zhuǎn)換總是得到基本標(biāo)量值的一種辩蛋,比如string呻畸,number移盆,或boolean悼院。沒有強(qiáng)制轉(zhuǎn)換可以得到像objectfunction這樣的復(fù)雜值。第三章講解了“封箱”咒循,它將一個(gè)基本類型標(biāo)量值包裝在它們相應(yīng)的object中据途,但在準(zhǔn)確的意義上這不是真正的強(qiáng)制轉(zhuǎn)換绞愚。

另一種區(qū)別這些術(shù)語的常見方法是:“類型轉(zhuǎn)換(type casting/conversion)”發(fā)生在靜態(tài)類型語言的編譯時(shí),而“類型強(qiáng)制轉(zhuǎn)換(type coercion)”是動(dòng)態(tài)類型語言的運(yùn)行時(shí)轉(zhuǎn)換颖医。

然而位衩,在JavaScript中,大多數(shù)人將所有這些類型的轉(zhuǎn)換都稱為 強(qiáng)制轉(zhuǎn)換(coercion)熔萧,所以我偏好的區(qū)別方式是使用“隱含強(qiáng)制轉(zhuǎn)換(implicit coercion)”與“明確強(qiáng)制轉(zhuǎn)換(explicit coercion)”糖驴。

其中的區(qū)別應(yīng)當(dāng)是很明顯的:在觀察代碼時(shí)如果一個(gè)類型轉(zhuǎn)換明顯是有意為之的,那么它就是“明確強(qiáng)制轉(zhuǎn)換”佛致,而如果這個(gè)類型轉(zhuǎn)換是做為其他操作的不那么明顯的副作用發(fā)生的贮缕,那么它就是“隱含強(qiáng)制轉(zhuǎn)換”。

例如俺榆,考慮這兩種強(qiáng)制轉(zhuǎn)換的方式:

var a = 42;

var b = a + "";         // 隱含強(qiáng)制轉(zhuǎn)換

var c = String( a );    // 明確強(qiáng)制轉(zhuǎn)換

對(duì)于b來說感昼,強(qiáng)制轉(zhuǎn)換是隱含地發(fā)生的,因?yàn)槿绻c+操作符組合的操作數(shù)之一是一個(gè)string值("")罐脊,這將使+操作成為一個(gè)string連接(將兩個(gè)字符串加在一起)定嗓,而string連接的 一個(gè)(隱藏的)副作用a中的值42強(qiáng)制轉(zhuǎn)換為它的string等價(jià)物:"42"

相比之下萍桌,String(..)函數(shù)使一切相當(dāng)明顯宵溅,它明確地取得a中的值,并把它強(qiáng)制轉(zhuǎn)換為一個(gè)string表現(xiàn)形式上炎。

兩種方式都能達(dá)到相同的效果:從42變成"42"层玲。但它們 如何 達(dá)到這種效果,才是關(guān)于JavaScript強(qiáng)制轉(zhuǎn)換的熱烈爭(zhēng)論的核心反症。

注意: 技術(shù)上講辛块,這里有一些在語法形式區(qū)別之上的,行為上的微妙區(qū)別铅碍。我們將在本章稍后润绵,“隱含:Strings <--> Numbers”一節(jié)中仔細(xì)講解。

“明確地”胞谈,“隱含地”尘盼,或“明顯地”和“隱藏的副作用”這些術(shù)語,是 相對(duì)的烦绳。

如果你確切地知道a + ""是在做什么卿捎,并且你有意地這么做來強(qiáng)制轉(zhuǎn)換一個(gè)string,你可能感覺這個(gè)操作已經(jīng)足夠“明確”了径密。相反午阵,如果你從沒見過String(..)函數(shù)被用于string強(qiáng)制轉(zhuǎn)換,那么對(duì)你來說它的行為可能看起來太過隱蔽而讓你感到“隱含”。

但我們是基于一個(gè) 大眾的底桂,充分了解植袍,但不是專家或JS規(guī)范愛好者的 開發(fā)者的觀點(diǎn)來討論“明確”與“隱含”的。無論你的程度如何籽懦,或是沒有在這個(gè)范疇內(nèi)準(zhǔn)確地找到自己于个,你都需要根據(jù)我們?cè)谶@里的觀察方式,相應(yīng)地調(diào)整你的角度暮顺。

記滋ā:我們自己寫代碼而也只有我們自己會(huì)讀它,通常是很少見的捶码。即便你是一個(gè)精通JS里里外外的專家敌土,也要考慮一個(gè)經(jīng)驗(yàn)沒那么豐富的隊(duì)友在讀你的代碼時(shí)感受如何坪圾。對(duì)于他們和對(duì)于你來說,“明確”或“隱含”的意義相同嗎?

抽象值操作

在我們可以探究 明確隱含 強(qiáng)制轉(zhuǎn)換之前疲眷,我們需要學(xué)習(xí)一些基本規(guī)則缨睡,是它們控制著值如何 變成 一個(gè)string矾端,number日熬,或boolean的。ES5語言規(guī)范的第9部分用值的變形規(guī)則定義了幾種“抽象操作”(“僅供內(nèi)部使用的操作”的高大上說法)盆繁。我們將特別關(guān)注于:ToString掀淘,ToNumber,和ToBoolean油昂,并稍稍關(guān)注一下ToPrimitive革娄。

ToString

當(dāng)任何一個(gè)非string值被強(qiáng)制轉(zhuǎn)換為一個(gè)string表現(xiàn)形式時(shí),這個(gè)轉(zhuǎn)換的過程是由語言規(guī)范的9.8部分的ToString抽象操作處理的冕碟。

內(nèi)建的基本類型值擁有自然的字符串化形式:null變?yōu)?code>"null"拦惋,undefined變?yōu)?code>"undefined",true變?yōu)?code>"true"安寺。number一般會(huì)以你期望的自然方式表達(dá)厕妖,但正如我們?cè)诘诙轮杏懻摰模浅P』蚍浅4蟮?code>number將會(huì)以指數(shù)形式表達(dá):

// `1.07`乘以`1000`挑庶,7次
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;

// 7次乘以3位 => 21位
a.toString(); // "1.07e21"

對(duì)于普通的對(duì)象言秸,除非你指定你自己的,默認(rèn)的toString()(可以在Object.prototype.toString()找到)將返回 內(nèi)部[[Class]](見第三章)迎捺,例如"[object Object]"举畸。

但正如早先所展示的,如果一個(gè)對(duì)象上擁有它自己的toString()方法凳枝,而你又以一種類似string的方式使用這個(gè)對(duì)象抄沮,那么它的toString()將會(huì)被自動(dòng)調(diào)用,而且這個(gè)調(diào)用的string結(jié)果將被使用。

注意: 技術(shù)上講合是,一個(gè)對(duì)象被強(qiáng)制轉(zhuǎn)換為一個(gè)string要通過ToPrimitive抽象操作(ES5語言規(guī)范,9.1部分)锭环,但是那其中的微妙細(xì)節(jié)將會(huì)在本章稍后的ToNumber部分中講解聪全,所以我們?cè)谶@里先跳過它。

數(shù)組擁有一個(gè)覆蓋版本的默認(rèn)toString()辅辩,將數(shù)組字符串化為它所有的值(每個(gè)都字符串化)的(字符串)連接难礼,并用","分割每個(gè)值。

var a = [1,2,3];

a.toString(); // "1,2,3"

重申一次玫锋,toString()可以明確地被調(diào)用蛾茉,也可以通過在一個(gè)需要string的上下文環(huán)境中使用一個(gè)非string來自動(dòng)地被調(diào)用。

JSON字符串化

另一種看起來與ToString密切相關(guān)的操作是撩鹿,使用JSON.stringify(..)工具將一個(gè)值序列化為一個(gè)JSON兼容的string值谦炬。

重要的是要注意,這種字符串化與強(qiáng)制轉(zhuǎn)換并不完全是同一種東西节沦。但是因?yàn)樗c上面講的ToString規(guī)則有關(guān)聯(lián)键思,我們將在這里稍微轉(zhuǎn)移一下話題,來講解JSON字符串化行為甫贯。

對(duì)于最簡(jiǎn)單的值吼鳞,JSON字符串化行為基本上和toString()轉(zhuǎn)換是相同的,除了序列化的結(jié)果 總是一個(gè)string

JSON.stringify( 42 );   // "42"
JSON.stringify( "42" ); // ""42"" (一個(gè)包含雙引號(hào)的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"

任何 JSON安全 的值都可以被JSON.stringify(..)字符串化叫搁。但是什么是 JSON安全的赔桌?任何可以用JSON表現(xiàn)形式合法表達(dá)的值。

考慮JSON 安全的值可能更容易一些渴逻。一些例子是:undefined疾党,function,(ES6+)symbol惨奕,和帶有循環(huán)引用的object(一個(gè)對(duì)象結(jié)構(gòu)中的屬性互相引用而造成了一個(gè)永不終結(jié)的循環(huán))仿贬。對(duì)于標(biāo)準(zhǔn)的JSON結(jié)構(gòu)來說這些都是非法的值,主要是因?yàn)樗鼈儾荒芤浦驳较M(fèi)JSON值的其他語言中墓贿。

JSON.stringify(..)工具在遇到undefined茧泪,function,和symbol時(shí)將會(huì)自動(dòng)地忽略它們聋袋。如果在一個(gè)array中遇到這樣的值队伟,它會(huì)被替換為null(這樣數(shù)組的位置信息就不會(huì)改變)。如果在一個(gè)object的屬性中遇到這樣的值幽勒,這個(gè)屬性會(huì)被簡(jiǎn)單地剔除掉嗜侮。

考慮下面的代碼:

JSON.stringify( undefined );                    // undefined
JSON.stringify( function(){} );                 // undefined

JSON.stringify( [1,undefined,function(){},4] ); // "[1,null,null,4]"
JSON.stringify( { a:2, b:function(){} } );      // "{"a":2}"

但如果你試著JSON.stringify(..)一個(gè)帶有循環(huán)引用的object,就會(huì)拋出一個(gè)錯(cuò)誤。

JSON字符串化有一個(gè)特殊行為锈颗,如果一個(gè)object值定義了一個(gè)toJSON()方法顷霹,這個(gè)方法將會(huì)被首先調(diào)用,以取得用于序列化的值击吱。

如果你打算JSON字符串化一個(gè)可能含有非法JSON值的對(duì)象淋淀,或者如果這個(gè)對(duì)象中正好有不適于序列化的值,那么你就應(yīng)當(dāng)為它定義一個(gè)toJSON()方法覆醇,返回這個(gè)object的一個(gè) JSON安全 版本朵纷。

例如:

var o = { };

var a = {
    b: 42,
    c: o,
    d: function(){}
};

// 在`a`內(nèi)部制造一個(gè)循環(huán)引用
o.e = a;

// 這回因循環(huán)引用而拋出一個(gè)錯(cuò)誤
// JSON.stringify( a );

// 自定義一個(gè)JSON值序列化
a.toJSON = function() {
    // 序列化僅包含屬性`b`
    return { b: this.b };
};

JSON.stringify( a ); // "{"b":42}"

一個(gè)很常見的誤解是,toJSON()應(yīng)當(dāng)返回一個(gè)JSON字符串化的表現(xiàn)形式永脓。這可能是不正確的袍辞,除非你事實(shí)上想要字符串化string本身(通常不會(huì)!)常摧。toJSON()應(yīng)當(dāng)返回合適的實(shí)際普通值(無論什么類型)搅吁,而JSON.stringify(..)自己會(huì)處理字符串化。

換句話說落午,toJSON()應(yīng)當(dāng)被翻譯為:“變?yōu)橐粋€(gè)適用于字符串化的JSON安全的值”似芝,不是像許多開發(fā)者錯(cuò)誤認(rèn)為的那樣,“變?yōu)橐粋€(gè)JSON字符串”板甘。

考慮下面的代碼:

var a = {
    val: [1,2,3],

    // 可能正確党瓮!
    toJSON: function(){
        return this.val.slice( 1 );
    }
};

var b = {
    val: [1,2,3],

    // 可能不正確!
    toJSON: function(){
        return "[" +
            this.val.slice( 1 ).join() +
        "]";
    }
};

JSON.stringify( a ); // "[2,3]"

JSON.stringify( b ); // ""[2,3]""

在第二個(gè)調(diào)用中盐类,我們字符串化了返回的string而不是array本身寞奸,這可能不是我們想要做的。

既然我們說到了JSON.stringify(..)在跳,那么就讓我們來討論一些不那么廣為人知枪萄,但是仍然很有用的功能吧。

JSON.stringify(..)的第二個(gè)參數(shù)值是可選的猫妙,它稱為 替換器(replacer)瓷翻。這個(gè)參數(shù)值既可以是一個(gè)array也可以是一個(gè)function。與toJSON()為序列化準(zhǔn)備一個(gè)值的方式類似割坠,它提供一種過濾機(jī)制齐帚,指出一個(gè)object的哪一個(gè)屬性應(yīng)該或不應(yīng)該被包含在序列化形式中,來自定義這個(gè)object的遞歸序列化行為彼哼。

如果 替換器 是一個(gè)array对妄,那么它應(yīng)當(dāng)是一個(gè)stringarray,它的每一個(gè)元素指定了允許被包含在這個(gè)object的序列化形式中的屬性名稱敢朱。如果一個(gè)屬性不存在于這個(gè)列表中剪菱,那么它就會(huì)被跳過摩瞎。

如果 替換器 是一個(gè)function,那么它會(huì)為object本身而被調(diào)用一次孝常,并且為這個(gè)object中的每個(gè)屬性都被調(diào)用一次旗们,而且每次都被傳入兩個(gè)參數(shù)值,keyvalue构灸。要在序列化中跳過一個(gè) key上渴,可以返回undefined。否則冻押,就返回被提供的 value驰贷。

var a = {
    b: 42,
    c: "42",
    d: [1,2,3]
};

JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"

JSON.stringify( a, function(k,v){
    if (k !== "c") return v;
} );
// "{"b":42,"d":[1,2,3]}"

注意:function替換器 的情況下盛嘿,第一次調(diào)用時(shí)key參數(shù)kundefined(而對(duì)象a本身會(huì)被傳入)洛巢。if語句會(huì) 過濾掉 名稱為c的屬性。字符串化是遞歸的次兆,所以數(shù)組[1,2,3]會(huì)將它的每一個(gè)值(1稿茉,2,和3)都作為v傳遞給 替換器芥炭,并將索引值(0漓库,1,和2)作為k园蝠。

JSON.stringify(..)還可以接收第三個(gè)可選參數(shù)值渺蒿,稱為 填充符(space),在對(duì)人類友好的輸出中它被用做縮進(jìn)彪薛。填充符 可以是一個(gè)正整數(shù)茂装,用來指示每一級(jí)縮進(jìn)中應(yīng)當(dāng)使用多少個(gè)空格字符∩蒲樱或者少态,填充符 可以是一個(gè)string,這時(shí)每一級(jí)縮進(jìn)將會(huì)使用它的前十個(gè)字符易遣。

var a = {
    b: 42,
    c: "42",
    d: [1,2,3]
};

JSON.stringify( a, null, 3 );
// "{
//    "b": 42,
//    "c": "42",
//    "d": [
//       1,
//       2,
//       3
//    ]
// }"

JSON.stringify( a, null, "-----" );
// "{
// -----"b": 42,
// -----"c": "42",
// -----"d": [
// ----------1,
// ----------2,
// ----------3
// -----]
// }"

記住彼妻,JSON.stringify(..)并不直接是一種強(qiáng)制轉(zhuǎn)換的形式。但是豆茫,我們?cè)谶@里討論它侨歉,是由于兩個(gè)與ToString強(qiáng)制轉(zhuǎn)換有關(guān)聯(lián)的行為:

  1. stringnumber揩魂,boolean为肮,和null值在JSON字符串化時(shí),與它們通過ToString抽象操作的規(guī)則強(qiáng)制轉(zhuǎn)換為string值的方式基本上是相同的肤京。
  2. 如果傳遞一個(gè)object值給JSON.stringify(..)颊艳,而這個(gè)object上擁有一個(gè)toJSON()方法茅特,那么在字符串化之前,toJSON()就會(huì)被自動(dòng)調(diào)用來將這個(gè)值(某種意義上)“強(qiáng)制轉(zhuǎn)換”為 JSON安全 的棋枕。

ToNumber

如果任何非number值白修,以一種要求它是number的方式被使用,比如數(shù)學(xué)操作重斑,就會(huì)發(fā)生ES5語言規(guī)范在9.3部分定義的ToNumber抽象操作兵睛。

例如,true變?yōu)?code>1而false變?yōu)?code>0窥浪。undefined變?yōu)?code>NaN祖很,而(奇怪的是)null變?yōu)?code>0。

對(duì)于一個(gè)string值來說漾脂,ToNumber工作起來很大程度上與數(shù)字字面量的規(guī)則/語法很相似(見第三章)假颇。如果它失敗了,結(jié)果將是NaN(而不是number字面量中會(huì)出現(xiàn)的語法錯(cuò)誤)骨稿。一個(gè)不同之處的例子是笨鸡,在這個(gè)操作中0前綴的八進(jìn)制數(shù)不會(huì)被作為八進(jìn)制數(shù)來處理(而僅作為普通的十進(jìn)制小數(shù)),雖然這樣的八進(jìn)制數(shù)作為number字面量是合法的坦冠。

注意: number字面量文法與用于string值的ToNumber間的區(qū)別極其微妙形耗,在這里就不進(jìn)一步講解了。更多的信息可以參考ES語言規(guī)范的9.3.1部分辙浑。

對(duì)象(以及數(shù)組)將會(huì)首先被轉(zhuǎn)換為它們的基本類型值的等價(jià)物激涤,而后這個(gè)結(jié)果值(如果它還不是一個(gè)number基本類型)會(huì)根據(jù)剛才提到的ToNumber規(guī)則被強(qiáng)制轉(zhuǎn)換為一個(gè)number

為了轉(zhuǎn)換為基本類型值的等價(jià)物判呕,ToPrimitive抽象操作(ES5語言規(guī)范倦踢,9.1部分)將會(huì)查詢這個(gè)值(使用內(nèi)部的DefaultValue操作 —— ES5語言規(guī)范,8.12.8部分)佛玄,看它有沒有valueOf()方法硼一。如果valueOf()可用并且它返回一個(gè)基本類型值,那么 這個(gè) 值就將用于強(qiáng)制轉(zhuǎn)換梦抢。如果不是這樣般贼,但toString()可用,那么就由它來提供用于強(qiáng)制轉(zhuǎn)換的值奥吩。

如果這兩種操作都沒提供一個(gè)基本類型值哼蛆,就會(huì)拋出一個(gè)TypeError

在ES5中霞赫,你可以創(chuàng)建這樣一個(gè)不可強(qiáng)制轉(zhuǎn)換的對(duì)象 —— 沒有valueOf()toString() —— 如果它的[[Prototype]]的值為null腮介,這通常是通過Object.create(null)來創(chuàng)建的。關(guān)于[[Prototype]]的詳細(xì)信息參見本系列的 this與對(duì)象原型端衰。

注意: 我們會(huì)在本章稍后講解如何強(qiáng)制轉(zhuǎn)換至number叠洗,但對(duì)于下面的代碼段甘改,想象Number(..)函數(shù)就是那樣做的。

考慮如下代碼:

var a = {
    valueOf: function(){
        return "42";
    }
};

var b = {
    toString: function(){
        return "42";
    }
};

var c = [4,2];
c.toString = function(){
    return this.join( "" ); // "42"
};

Number( a );            // 42
Number( b );            // 42
Number( c );            // 42
Number( "" );           // 0
Number( [] );           // 0
Number( [ "abc" ] );    // NaN

ToBoolean

下面灭抑,讓我們聊一聊在JS中boolean如何動(dòng)作十艾。世面上關(guān)于這個(gè)話題有 許多的困惑和誤解,所以集中注意力腾节!

首先而且最重要的是忘嫉,JS實(shí)際上擁有truefalse關(guān)鍵字,而且它們的行為正如你所期望的boolean值一樣案腺。一個(gè)常見的誤解是庆冕,值10true/false是相同的。雖然這可能在其他語言中是成立的劈榨,但在JS中number就是number访递,而boolean就是boolean。你可以將1強(qiáng)制轉(zhuǎn)換為true(或反之)鞋既,或?qū)?code>0強(qiáng)制轉(zhuǎn)換為false(或反之)力九。但它們不是相同的耍铜。

Falsy值

但這還不是故事的結(jié)尾邑闺。我們需要討論一下,除了這兩個(gè)boolean值以外棕兼,當(dāng)你把其他值強(qiáng)制轉(zhuǎn)換為它們的boolean等價(jià)物時(shí)如何動(dòng)作陡舅。

所有的JavaScript值都可以被劃分進(jìn)兩個(gè)類別:

  1. 如果被強(qiáng)制轉(zhuǎn)換為boolean,將成為false的值
  2. 其它的一切值(很明顯將變?yōu)?code>true)

我不是在出洋相伴挚。JS語言規(guī)范給那些在強(qiáng)制轉(zhuǎn)換為boolean值時(shí)將會(huì)變?yōu)?code>false的值定義了一個(gè)明確的靶衍,小范圍的列表。

我們?nèi)绾尾拍苤肋@個(gè)列表中的值是什么茎芋?在ES5語言規(guī)范中颅眶,9.2部分定義了一個(gè)ToBoolean抽象操作,它講述了對(duì)所有可能的值而言田弥,當(dāng)你試著強(qiáng)制轉(zhuǎn)換它們?yōu)閎oolean時(shí)究竟會(huì)發(fā)生什么涛酗。

從這個(gè)表格中,我們得到了下面所謂的“falsy”值列表:

  • undefined
  • null
  • false
  • +0, -0, and NaN
  • ""

就是這些偷厦。如果一個(gè)值在這個(gè)列表中商叹,它就是一個(gè)“falsy”值,而且當(dāng)你在它上面進(jìn)行boolean強(qiáng)制轉(zhuǎn)換時(shí)它會(huì)轉(zhuǎn)換為false只泼。

通過邏輯上的推論剖笙,如果一個(gè)值 在這個(gè)列表中,那么它一定在 另一個(gè)列表 中请唱,也就是我們稱為“truthy”值的列表弥咪。但是JS沒有真正定義一個(gè)“truthy”列表过蹂。它給出了一些例子,比如它說所有的對(duì)象都是truthy聚至,但是語言規(guī)范大致上暗示著:任何沒有明確地存在于falsy列表中的東西榴啸,都是truthy

Falsy對(duì)象

等一下晚岭,這一節(jié)的標(biāo)題聽起來簡(jiǎn)直是矛盾的鸥印。我 剛剛才說過 語言規(guī)范將所有對(duì)象稱為truthy,對(duì)吧坦报?應(yīng)該沒有“falsy對(duì)象”這樣的東西库说。

這會(huì)是什么意思呢?

它可能誘使你認(rèn)為它意味著一個(gè)包裝了falsy值(比如""片择,0false)的對(duì)象包裝器(見第三章)潜的。但別掉到這個(gè) 陷阱 中。

注意: 這個(gè)可能是一個(gè)語言規(guī)范的微妙笑話字管。

考慮下面的代碼:

var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );

我們知道這三個(gè)值都是包裝了明顯是falsy值的對(duì)象(見第三章)啰挪。但這些對(duì)象是作為true還是作為false動(dòng)作呢?這很容易回答:

var d = Boolean( a && b && c );

d; // true

所以嘲叔,三個(gè)都作為true動(dòng)作亡呵,這是唯一能使d得到true的方法。

提示: 注意包在a && b && c表達(dá)式外面的Boolean( .. ) —— 你可能想知道為什么它在這兒硫戈。我們會(huì)在本章稍后回到這個(gè)話題锰什,所以先做個(gè)心理準(zhǔn)備。為了先睹為快丁逝,你可以自己試試如果沒有Boolean( .. )調(diào)用而只有d = a && b && c時(shí)d是什么汁胆。

那么,如果“falsy對(duì)象” 不是包裝著falsy值的對(duì)象霜幼,它們是什么鬼東西嫩码?

刁鉆的地方在于,它們可以出現(xiàn)在你的JS程序中罪既,但它們實(shí)際上不是JavaScript本身的一部分铸题。

什么!萝衩?

有些特定的情況回挽,在普通的JS語義之上,瀏覽器已經(jīng)創(chuàng)建了它們自己的某種 外來 值的行為猩谊,也就是這種“falsy對(duì)象”的想法千劈。

一個(gè)“falsy對(duì)象”看起來和動(dòng)起來都像一個(gè)普通對(duì)象(屬性,等等)的值牌捷,但是當(dāng)你強(qiáng)制轉(zhuǎn)換它為一個(gè)boolean時(shí)墙牌,它會(huì)變?yōu)橐粋€(gè)false值涡驮。

為什么!喜滨?

最著名的例子是document.all:一個(gè) 由DOM(不是JS引擎本身) 給你的JS程序提供的類數(shù)組(對(duì)象)捉捅,它向你的JS程序暴露你頁(yè)面上的元素。它 曾經(jīng) 像一個(gè)普通對(duì)象那樣動(dòng)作 —— 是一個(gè)truthy虽风。但不再是了棒口。

document.all本身從來就不是“標(biāo)準(zhǔn)的”,而且從很早以前就被廢棄/拋棄了辜膝。

“那他們就不能刪掉它嗎无牵?” 對(duì)不起,想得不錯(cuò)厂抖。但愿它們能茎毁。但是世面上有太多的遺產(chǎn)JS代碼庫(kù)依賴于它。

那么忱辅,為什么使它像falsy一樣動(dòng)作七蜘?因?yàn)閺?code>document.all到boolean的強(qiáng)制轉(zhuǎn)換(比如在if語句中)幾乎總是用來檢測(cè)老的,非標(biāo)準(zhǔn)的IE墙懂。

IE從很早以前就開始順應(yīng)規(guī)范了橡卤,而且在許多情況下它在推動(dòng)web向前發(fā)展的作用和其他瀏覽器一樣多,甚至更多垒在。但是所有那些老舊的if (document.all) { /* it's IE */ }代碼依然留在世面上蒜魄,而且大多數(shù)可能永遠(yuǎn)都不會(huì)消失扔亥。所有這些遺產(chǎn)代碼依然假設(shè)它們運(yùn)行在那些給IE用戶帶來差勁兒的瀏覽體驗(yàn)的场躯,幾十年前的老IE上,

所以旅挤,我們不能完全移除document.all踢关,但是IE不再想讓if (document.all) { .. }代碼繼續(xù)工作了,這樣現(xiàn)代IE的用戶就能得到新的粘茄,符合標(biāo)準(zhǔn)的代碼邏輯签舞。

“我們應(yīng)當(dāng)怎么做?” “我知道了柒瓣!讓我們黑進(jìn)JS的類型系統(tǒng)并假裝document.all是falsy儒搭!”

呃。這很爛芙贫。這是一個(gè)大多數(shù)JS開發(fā)者們都不理解的瘋狂的坑搂鲫。但是其它的替代方案(對(duì)上面兩敗俱傷的問題什么都不做)還要爛得 多那么一點(diǎn)點(diǎn)

所以……這就是我們得到的:由瀏覽器給JavaScript添加的瘋狂磺平,非標(biāo)準(zhǔn)的“falsy對(duì)象”魂仍。耶拐辽!

Truthy值

回到truthy列表。到底什么是truthy值擦酌?記拙阒睢:如果一個(gè)值不在falsy列表中,它就是truthy赊舶。

考慮下面代碼:

var a = "false";
var b = "0";
var c = "''";

var d = Boolean( a && b && c );

d;

你期望這里的d是什么值睁搭?它要么是true要么是false

它是true笼平。為什么介袜?因?yàn)楸M管這些string值的內(nèi)容看起來是falsy值,但是string值本身都是truthy出吹,而這是因?yàn)樵趂alsy列表中""是唯一的string值遇伞。

那么這些呢?

var a = [];             // 空數(shù)組 -- truthy 還是 falsy?
var b = {};             // 空對(duì)象 -- truthy 還是 falsy?
var c = function(){};   // 空函數(shù) -- truthy 還是 falsy?

var d = Boolean( a && b && c );

d;

是的捶牢,你猜到了鸠珠,這里的d依然是true。為什么秋麸?和前面的原因一樣渐排。盡管它們看起來像,但是[]灸蟆,{}驯耻,和function(){} 不在 falsy列表中,因此它們是truthy值炒考。

換句話說可缚,truthy列表是無限長(zhǎng)的。不可能制成一個(gè)這樣的列表斋枢。你只能制造一個(gè)falsy列表并查詢它帘靡。

花五分鐘,把falsy列表寫在便利貼上瓤帚,然后粘在你的電腦顯示器上描姚,或者如果你愿意就記住它。不管哪種方法戈次,你都可以在自己需要的時(shí)候通過簡(jiǎn)單地查詢一個(gè)值是否在falsy列表中轩勘,來構(gòu)建一個(gè)虛擬的truthy列表。

truthy和falsy的重要性在于怯邪,理解如果一個(gè)值在被(明確地或隱含地)強(qiáng)制轉(zhuǎn)換為boolean值的話绊寻,它將如何動(dòng)作。現(xiàn)在你的大腦中有了這兩個(gè)列表,我們可以深入強(qiáng)制轉(zhuǎn)換的例子本身了榛斯。

明確的強(qiáng)制轉(zhuǎn)換

明確的 強(qiáng)制轉(zhuǎn)換指的是明顯且明確的類型轉(zhuǎn)換观游。對(duì)于大多數(shù)開發(fā)者來說,有很多類型轉(zhuǎn)換的用法可以清楚地歸類于這種 明確的 強(qiáng)制轉(zhuǎn)換驮俗。

我們?cè)谶@里的目標(biāo)是懂缕,在我們的代碼中指明一些模式,在這些模式中我們可以清楚明白地將一個(gè)值從一種類型轉(zhuǎn)換至另一種類型王凑,以確保不給未來將讀到這段代碼的開發(fā)者留下任何坑搪柑。我們?cè)矫鞔_,后來的人就越容易讀懂我們的代碼索烹,也不必費(fèi)太多的力氣去理解我們的意圖工碾。

關(guān)于 明確的 強(qiáng)制轉(zhuǎn)換可能很難找到什么主要的不同意見,因?yàn)樗c被廣泛接受的靜態(tài)類型語言中的類型轉(zhuǎn)換的工作方式非常接近百姓。因此渊额,我們理所當(dāng)然地認(rèn)為(暫且) 明確的 強(qiáng)制轉(zhuǎn)換可以被認(rèn)同為不是邪惡的,或沒有爭(zhēng)議的垒拢。雖然我們稍后會(huì)回到這個(gè)話題旬迹。

明確地:Strings <--> Numbers

我們將從最簡(jiǎn)單,也許是最常見強(qiáng)制轉(zhuǎn)換操作開始:將值在stringnumber表現(xiàn)形式之間進(jìn)行強(qiáng)制轉(zhuǎn)換求类。

為了在stringnumber之間進(jìn)行強(qiáng)制轉(zhuǎn)換奔垦,我們使用內(nèi)建的String(..)Number(..)函數(shù)(我們?cè)诘谌轮兴傅摹霸鷺?gòu)造器”),但 非常重要的是尸疆,我們不在它們前面使用new關(guān)鍵字椿猎。這樣,我們就不是在創(chuàng)建對(duì)象包裝器寿弱。

取而代之的是犯眠,我們實(shí)際上在兩種類型之間進(jìn)行 明確地強(qiáng)制轉(zhuǎn)換

var a = 42;
var b = String( a );

var c = "3.14";
var d = Number( c );

b; // "42"
d; // 3.14

String(..)使用早先討論的ToString操作的規(guī)則,將任意其它的值強(qiáng)制轉(zhuǎn)換為一個(gè)基本類型的string值脖捻。Number(..)使用早先討論過的ToNumber操作的規(guī)則阔逼,將任意其他的值強(qiáng)制轉(zhuǎn)換為一個(gè)基本類型的number值。

我稱此為 明確的 強(qiáng)制轉(zhuǎn)換是因?yàn)榈鼐冢话銓?duì)于大多數(shù)開發(fā)者來說這是十分明顯的:這些操作的最終結(jié)果是適當(dāng)?shù)念愋娃D(zhuǎn)換。

實(shí)際上羡亩,這種用法看起來與其他的靜態(tài)類型語言中的用法非常相像摩疑。

舉個(gè)例子,在C/C++中畏铆,你既可以說(int)x也可以說int(x)雷袋,而且它們都將x中的值轉(zhuǎn)換為一個(gè)整數(shù)。兩種形式都是合法的,但是許多人偏向于后者楷怒,它看起來有點(diǎn)兒像一個(gè)函數(shù)調(diào)用蛋勺。在JavaScript中,當(dāng)你說Number(x)時(shí)鸠删,它看起來極其相似抱完。在JS中它實(shí)際上是一個(gè)函數(shù)調(diào)用這個(gè)事實(shí)重要嗎?并非如此刃泡。

除了String(..)Number(..)巧娱,還有其他的方法可以把這些值在stringnumber之間進(jìn)行“明確地”轉(zhuǎn)換:

var a = 42;
var b = a.toString();

var c = "3.14";
var d = +c;

b; // "42"
d; // 3.14

調(diào)用a.toString()在表面上是明確的(“toString”意味著“變成一個(gè)字符串”是很明白的),但是這里有一些藏起來的隱含性烘贴。toString()不能在像42這樣的 基本類型 值上調(diào)用禁添。所以JS會(huì)自動(dòng)地將42“封箱”在一個(gè)對(duì)象包裝器中(見第三章),這樣toString()就可以針對(duì)這個(gè)對(duì)象調(diào)用桨踪。換句話講老翘,你可能會(huì)叫它“明確的隱含”崎页。

這里的+c+操作符的 一元操作符(操作符只有一個(gè)操作數(shù))形式美旧。取代進(jìn)行數(shù)學(xué)加法(或字符串連接 —— 見下面的討論)的是,一元的+明確地將它的操作數(shù)(c)強(qiáng)制轉(zhuǎn)換為一個(gè)number值尸红。

+c明確的 強(qiáng)制轉(zhuǎn)換嗎纳账?這要看你的經(jīng)驗(yàn)和角度逛薇。如果你知道(現(xiàn)在你知道了!)一元+明確地意味著number強(qiáng)制轉(zhuǎn)換疏虫,那么它就是相當(dāng)明確和明顯的永罚。但是,如果你以前從沒見過它卧秘,那么它看起來就極其困惑呢袱,晦澀,帶有隱含的副作用翅敌,等等羞福。

注意: 在開源的JS社區(qū)中一般被接受的觀點(diǎn)是,一元+是一個(gè) 明確的 強(qiáng)制轉(zhuǎn)換形式蚯涮。

即使你真的喜歡+c這種形式治专,它絕對(duì)會(huì)在有的地方看起來非常令人困惑≡舛ィ考慮下面的代碼:

var c = "3.14";
var d = 5+ +c;

d; // 8.14

一元-操作符也像+一樣進(jìn)行強(qiáng)制轉(zhuǎn)換张峰,但它還會(huì)翻轉(zhuǎn)數(shù)字的符號(hào)。但是你不能放兩個(gè)減號(hào)--來使符號(hào)翻轉(zhuǎn)回來棒旗,因?yàn)槟菍⒈唤忉尀檫f減操作符喘批。取代它的是,你需要這么做:- -"3.14",在兩個(gè)減號(hào)之間加入空格饶深,這將會(huì)使強(qiáng)制轉(zhuǎn)換的結(jié)果為3.14餐曹。

你可能會(huì)想到所有種類的可怕組合 —— 一個(gè)二元操作符挨著另一個(gè)操作符的一元形式。這里有另一個(gè)瘋狂的例子:

1 + - + + + - + 1;  // 2

當(dāng)一個(gè)一元+(或-)緊鄰其他操作符時(shí)敌厘,你應(yīng)當(dāng)強(qiáng)烈地考慮避免使用它台猴。雖然上面的代碼可以工作,但幾乎全世界都認(rèn)為它是一個(gè)壞主意额湘。即使是d = +c(或者d =+ cG渫隆)都太容易與d += c像混淆了,而后者完全是不同的東西锋华!

注意: 一元+的另一個(gè)極端使人困惑的地方是嗡官,被用于緊挨著另一個(gè)將要作為++遞增操作符和--遞減操作符的操作數(shù)。例如:a +++b毯焕,a + ++b衍腥,和a + + +b。更多關(guān)于++的信息纳猫,參見第五章的“表達(dá)式副作用”婆咸。

記住,我們正努力變得明確并 減少 困惑芜辕,不是把事情弄得更糟尚骄!

Datenumber

另一個(gè)一元+操作符的常見用法是將一個(gè)Date對(duì)象強(qiáng)制轉(zhuǎn)換為一個(gè)number,其結(jié)果是這個(gè)日期/時(shí)間值的unix時(shí)間戳(從世界協(xié)調(diào)時(shí)間的1970年1月1日0點(diǎn)開始計(jì)算侵续,經(jīng)過的毫秒數(shù))表現(xiàn)形式:

var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );

+d; // 1408369986000

這種習(xí)慣性用法經(jīng)常用于取得當(dāng)前的 現(xiàn)在 時(shí)刻的時(shí)間戳倔丈,比如:

var timestamp = +new Date();

注意: 一些開發(fā)者知道一個(gè)JavaScript中的特別的語法“技巧”,就是在構(gòu)造器調(diào)用(一個(gè)帶有new的函數(shù)調(diào)用)中如果沒有參數(shù)值要傳遞的話状蜗,()可選的需五。所以你可能遇到var timestamp = +new Date;形式。然而轧坎,不是所有的開發(fā)者都同意忽略()可以增強(qiáng)可讀性宏邮,因?yàn)樗且环N不尋常的語法特例,只能適用于new fn()調(diào)用形式缸血,而不能用于普通的fn()調(diào)用形式蜜氨。

但強(qiáng)制轉(zhuǎn)換不是從Date對(duì)象中取得時(shí)間戳的唯一方法。一個(gè)不使用強(qiáng)制轉(zhuǎn)換的方式可能更好属百,因?yàn)樗用鞔_:

var timestamp = new Date().getTime();
// var timestamp = (new Date()).getTime();
// var timestamp = (new Date).getTime();

但是一個(gè) 更更好的 不使用強(qiáng)制轉(zhuǎn)換的選擇是使用ES5加入的Date.now()靜態(tài)函數(shù):

var timestamp = Date.now();

而且如果你想要為老版本的瀏覽器填補(bǔ)Date.now()的話记劝,也十分簡(jiǎn)單:

if (!Date.now) {
    Date.now = function() {
        return +new Date();
    };
}

我推薦跳過與日期有關(guān)的強(qiáng)制轉(zhuǎn)換形式。使用Date.now()來取得當(dāng)前 現(xiàn)在 的時(shí)間戳族扰,而使用new Date( .. ).getTime()來取得一個(gè)需要你指定的 非現(xiàn)在 日期/時(shí)間的時(shí)間戳。

奇異的~

一個(gè)經(jīng)常被忽視并通常讓人糊涂的JS強(qiáng)制操作符是波浪線~操作符(也叫“按位取反”,“比特非”)渔呵。許多理解它在做什么的人也總是想要避開它怒竿。但是為了堅(jiān)持我們?cè)诒緯捅鞠盗兄械木瘢屛覀兩钊氩⒄页?code>~是否有一些對(duì)我們有用的東西扩氢。

在第二章的“32位(有符號(hào))整數(shù)”一節(jié)耕驰,我們講解了在JS中位操作符是如何僅為32位操作定義的,這意味著我們強(qiáng)制它們的操作數(shù)遵循32位值的表現(xiàn)形式录豺。這個(gè)規(guī)則如何發(fā)生是由ToInt32抽象操作(ES5語言規(guī)范朦肘,9.5部分)控制的。

ToInt32首先進(jìn)行ToNumber強(qiáng)制轉(zhuǎn)換双饥,這就是說如果值是"123"媒抠,它在ToInt32規(guī)則實(shí)施之前會(huì)首先變成123

雖然它本身沒有 技術(shù)上進(jìn)行 強(qiáng)制轉(zhuǎn)換(因?yàn)轭愋蜎]有改變)咏花,但對(duì)一些特定的特殊number值使用位操作符(比如|~)會(huì)產(chǎn)生一種強(qiáng)制轉(zhuǎn)換效果趴生,這種效果的結(jié)果是一個(gè)不同的number值。

舉例來說昏翰,讓我們首先考慮慣用的空操作0 | x(在第二種章有展示)中使用的|“比特或”操作符苍匆,它實(shí)質(zhì)上僅僅進(jìn)行ToInt32轉(zhuǎn)換:

0 | -0;         // 0
0 | NaN;        // 0
0 | Infinity;   // 0
0 | -Infinity;  // 0

這些特殊的數(shù)字是不可用32位表現(xiàn)的(因?yàn)樗鼈冊(cè)醋?4位的IEEE 754標(biāo)準(zhǔn) —— 見第二章),所以ToInt32將這些值的結(jié)果指定為0棚菊。

有爭(zhēng)議的是浸踩,0 | __是否是一種ToInt32強(qiáng)制轉(zhuǎn)換操作的 明確的 形式,還是更傾向于 隱含统求。從語言規(guī)范的角度來說检碗,毫無疑問是 明確的,但是如果你沒有在這樣的層次上理解位操作球订,它就可能看起來有點(diǎn)像 隱含的 魔法后裸。不管怎樣,為了與本章中其他的斷言保持一致冒滩,我們稱它為 明確的微驶。

那么,讓我們把注意力轉(zhuǎn)回~开睡。~操作符首先將值“強(qiáng)制轉(zhuǎn)換”為一個(gè)32位number值因苹,然后實(shí)施按位取反(翻轉(zhuǎn)每一個(gè)比特位)。

注意: 這與!不僅強(qiáng)制轉(zhuǎn)換它的值為boolean而且還翻轉(zhuǎn)它的每一位很相似(見后面關(guān)于“一元!”的討論)篇恒。

但是……什么7鲩堋?為什么我們要關(guān)心被翻轉(zhuǎn)的比特位胁艰?這是一些相當(dāng)特殊的款筑,微妙的東西智蝠。JS開發(fā)者需要推理個(gè)別比特位是十分少見的。

另一種考慮~定義的方法是奈梳,~源自學(xué)校中的計(jì)算機(jī)科學(xué)/離散數(shù)學(xué):~進(jìn)行二進(jìn)制取補(bǔ)操作杈湾。太好了,謝謝攘须,我完全明白了漆撞!

我們?cè)僭囈淮危?code>~x大致與-(x+1)相同。這很奇怪于宙,但是稍微容易推理一些浮驳。所以:

~42;    // -(42+1) ==> -43

你可能還在想~這個(gè)鬼東西到底和什么有關(guān),或者對(duì)于強(qiáng)制轉(zhuǎn)換的討論它究竟有什么要緊捞魁。讓我們快速進(jìn)入要點(diǎn)至会。

考慮一下-(x+1)。通過進(jìn)行這個(gè)操作署驻,能夠產(chǎn)生結(jié)果0(或者從技術(shù)上說-07芟住)的唯一的值是什么?-1旺上。換句話說瓶蚂,~用于一個(gè)范圍的number值時(shí),將會(huì)為輸入值-1產(chǎn)生一個(gè)falsy(很容易強(qiáng)制轉(zhuǎn)換為false)的0宣吱,而為任意其他的輸入產(chǎn)生truthy的number窃这。

為什么這要緊?

-1通常稱為一個(gè)“哨兵值”征候,它基本上意味著一個(gè)在同類型值(number)的更大的集合中被賦予了任意的語義杭攻。在C語言中許多函數(shù)使用哨兵值-1,它們返回>= 0的值表示“成功”疤坝,返回-1表示“失敗”兆解。

JavaScript在定義string操作indexOf(..)時(shí)采納了這種先例,它搜索一個(gè)子字符串跑揉,如果找到就返回它從0開始計(jì)算的索引位置锅睛,沒有找到的話就返回-1

這樣的情況很常見:不僅僅將indexOf(..)作為取得位置的操作历谍,而且作為檢查一個(gè)子字符串存在/不存在于另一個(gè)string中的boolean值现拒。這就是開發(fā)者們通常如何進(jìn)行這樣的檢查:

var a = "Hello World";

if (a.indexOf( "lo" ) >= 0) {   // true
    // 找到了!
}
if (a.indexOf( "lo" ) != -1) {  // true
    // 找到了
}

if (a.indexOf( "ol" ) < 0) {    // true
    // 沒找到望侈!
}
if (a.indexOf( "ol" ) == -1) {  // true
    // 沒找到印蔬!
}

我感覺看著>= 0== -1有些惡心。它基本上是一種“抽象泄漏”脱衙,這里它將底層的實(shí)現(xiàn)行為 —— 使用哨兵值-1表示“失敗” —— 泄漏到我的代碼中侥猬。我倒是樂意隱藏這樣的細(xì)節(jié)例驹。

現(xiàn)在,我們終于看到為什~可以幫到我們了陵究!將~indexOf()一起使用可以將值“強(qiáng)制轉(zhuǎn)換”(實(shí)際上只是變形)為 可以適當(dāng)?shù)貜?qiáng)制轉(zhuǎn)換為boolean的值

var a = "Hello World";

~a.indexOf( "lo" );         // -4   <-- truthy!

if (~a.indexOf( "lo" )) {   // true
    // 找到了眠饮!
}

~a.indexOf( "ol" );         // 0    <-- falsy!
!~a.indexOf( "ol" );        // true

if (!~a.indexOf( "ol" )) {  // true
    // 沒找到奥帘!
}

~拿到indexOf(..)的返回值并將它變形:對(duì)于“失敗”的-1我們得到falsy的0铜邮,而其他的值都是truthy。

注意: ~的假想算法-(x+1)暗示著~-1-0寨蹋,但是實(shí)際上它產(chǎn)生0松蒜,因?yàn)榈讓拥牟僮髌鋵?shí)是按位的,不是數(shù)學(xué)操作已旧。

技術(shù)上講秸苗,if (~a.indexOf(..))仍然依靠 隱含的 強(qiáng)制轉(zhuǎn)換將它的結(jié)果0變?yōu)?code>false或非零變?yōu)?code>true。但總的來說运褪,對(duì)我而言~更像一種 明確的 強(qiáng)制轉(zhuǎn)換機(jī)制惊楼,只要你知道在這種慣用法中它的意圖是什么。

我感覺這樣的代碼要比前面凌亂的>= 0 / == -1更干凈秸讹。

截?cái)啾忍匚?/h5>

在你遇到的代碼中檀咙,還有一個(gè)地方可能出現(xiàn)~:一些開發(fā)者使用雙波浪線~~來截?cái)嘁粋€(gè)number的小數(shù)部分(也就是,將它“強(qiáng)制轉(zhuǎn)換”為一個(gè)“整數(shù)”)璃诀。這通常(雖然是錯(cuò)誤的)被說成與調(diào)用Math.floor(..)的結(jié)果相同弧可。

~ ~的工作方式是,第一個(gè)~實(shí)施ToInt32“強(qiáng)制轉(zhuǎn)換”并進(jìn)行按位取反劣欢,然后第二個(gè)~進(jìn)行另一次按位取反棕诵,將每一個(gè)比特位都翻轉(zhuǎn)回原來的狀態(tài)。于是最終的結(jié)果就是ToInt32“強(qiáng)制轉(zhuǎn)換”(也叫截?cái)啵?/p>

注意: ~~的按位雙翻轉(zhuǎn)凿将,與雙否定!!的行為非常相似校套,它將在稍后的“明確地:* --> Boolean”一節(jié)中講解。

然而牧抵,~~需要一些注意/澄清笛匙。首先,它僅在32位值上可以可靠地工作灭忠。但更重要的是膳算,它在負(fù)數(shù)上工作的方式與Math.floor(..)不同!

Math.floor( -49.6 );    // -50
~~-49.6;                // -49

Math.floor(..)的不同放在一邊弛作,~~x可以將值截?cái)酁橐粋€(gè)(32位)整數(shù)涕蜂。但是x | 0也可以,而且看起來還(稍微)省事兒 一些映琳。

那么机隙,為什么你可能會(huì)選擇~~x而不是x | 0蜘拉?操作符優(yōu)先權(quán)(見第五章):

~~1E20 / 10;        // 166199296

1E20 | 0 / 10;      // 1661992960
(1E20 | 0) / 10;    // 166199296

正如這里給出的其他建議一樣,僅在讀/寫這樣的代碼的每一個(gè)人都知道這些操作符如何工作的情況下有鹿,才將~~~作為“強(qiáng)制轉(zhuǎn)換”和將值變形的明確機(jī)制旭旭。

明確地:解析數(shù)字字符串

將一個(gè)string強(qiáng)制轉(zhuǎn)換為一個(gè)number的類似結(jié)果,可以通過從string的字符內(nèi)容中解析(parsing)出一個(gè)number得到葱跋。然而在這種解析和我們上面講解的類型轉(zhuǎn)換之間存在著區(qū)別持寄。

考慮下面的代碼:

var a = "42";
var b = "42px";

Number( a );    // 42
parseInt( a );  // 42

Number( b );    // NaN
parseInt( b );  // 42

從一個(gè)字符串中解析出一個(gè)數(shù)字是 容忍 非數(shù)字字符的 —— 從左到右,如果遇到非數(shù)字字符就停止解析 —— 而強(qiáng)制轉(zhuǎn)換是 不容忍 并且會(huì)失敗而得出值NaN娱俺。

解析不應(yīng)當(dāng)被視為強(qiáng)制轉(zhuǎn)換的替代品稍味。這兩種任務(wù)雖然相似,但是有著不同的目的荠卷。當(dāng)你不知道/不關(guān)心右手邊可能有什么其他的非數(shù)字字符時(shí)模庐,你可以將一個(gè)string作為number解析。當(dāng)只有數(shù)字才是可接受的值油宜,而且像"42px"這樣的東西作為數(shù)字應(yīng)當(dāng)被排除時(shí)掂碱,就強(qiáng)制轉(zhuǎn)換一個(gè)string(變?yōu)橐粋€(gè)number)。

提示: parseInt(..)有一個(gè)孿生兄弟慎冤,parseFloat(..)疼燥,它(聽起來)從一個(gè)字符串中拉出一個(gè)浮點(diǎn)數(shù)。

不要忘了parseInt(..)工作在string值上粪薛。向parseInt(..)傳遞一個(gè)number絕對(duì)沒有任何意義悴了。傳遞其他任何類型也都沒有意義,比如true违寿, function(){..}[1,2,3]湃交。

如果你傳入一個(gè)非string,你所傳入的值首先將自動(dòng)地被強(qiáng)制轉(zhuǎn)換為一個(gè)string(見早先的“ToString”)藤巢,這很明顯是一種隱藏的 隱含 強(qiáng)制轉(zhuǎn)換搞莺。在你的程序中依賴這樣的行為真的是一個(gè)壞主意,所以永遠(yuǎn)也不要將parseInt(..)與非string值一起使用掂咒。

在ES5之前才沧,parseInt(..)還存在另外一個(gè)坑,這曾是許多JS程序的bug的根源绍刮。如果你不傳遞第二個(gè)參數(shù)來指定使用哪種進(jìn)制(也叫基數(shù))來翻譯數(shù)字的string內(nèi)容温圆,parseInt(..)將會(huì)根據(jù)開頭的字符進(jìn)行猜測(cè)。

如果開頭的兩個(gè)字符是"0x""0X"孩革,那么猜測(cè)(根據(jù)慣例)將是你想要將這個(gè)string翻譯為一個(gè)16進(jìn)制number岁歉。否則,如果第一個(gè)字符是"0"膝蜈,那么猜測(cè)(也是根據(jù)慣例)將是你想要將這個(gè)string翻譯成8進(jìn)制number锅移。

16進(jìn)制的string(以0x0X開頭)沒那么容易搞混熔掺。但是事實(shí)證明8進(jìn)制數(shù)字的猜測(cè)過于常見了。比如:

var hour = parseInt( selectedHour.value );
var minute = parseInt( selectedMinute.value );

console.log( "The time you selected was: " + hour + ":" + minute);

看起來無害非剃,對(duì)吧置逻?試著在小時(shí)上選擇08在分鐘上選擇09。你會(huì)得到0:0备绽。為什么券坞?因?yàn)?code>8和9都不是合法的8進(jìn)制數(shù)。

ES5之前的修改很簡(jiǎn)單疯坤,但是很容易忘:總是在第二個(gè)參數(shù)值上傳遞10报慕。這完全是安全的:

var hour = parseInt( selectedHour.value, 10 );
var minute = parseInt( selectedMiniute.value, 10 );

在ES5中,parseInt(..)不再猜測(cè)八進(jìn)制數(shù)了压怠。除非你指定,否則它會(huì)假定為10進(jìn)制(或者為"0x"前綴猜測(cè)16進(jìn)制數(shù))飞苇。這好多了菌瘫。只是要小心,如果你的代碼不得不運(yùn)行在前ES5環(huán)境中布卡,你仍然需要為基數(shù)傳遞10雨让。

解析非字符串

幾年以前有一個(gè)挖苦JS的玩笑,使一個(gè)關(guān)于parseInt(..)行為的一個(gè)臭名昭著的例子備受關(guān)注忿等,它取笑JS的這個(gè)行為:

parseInt( 1/0, 19 ); // 18

這里面設(shè)想(但完全不合法)的斷言是栖忠,“如果我傳入一個(gè)無限大,并從中解析出一個(gè)整數(shù)的話贸街,我應(yīng)該得到一個(gè)無限大庵寞,不是18”。沒錯(cuò)薛匪,JS一定是瘋了才得出這個(gè)結(jié)果捐川,對(duì)吧?

雖然這是個(gè)明顯故意造成的逸尖,不真實(shí)的例子古沥,但是讓我們放縱這種瘋狂一小會(huì)兒,來檢視一下JS是否真的那么瘋狂娇跟。

首先岩齿,這其中最明顯的原罪是將一個(gè)非string傳入了parseInt(..)。這是不對(duì)的苞俘。這么做是自找麻煩盹沈。但就算你這么做了,JS也會(huì)禮貌地將你傳入的東西強(qiáng)制轉(zhuǎn)換為它可以解析的string苗胀。

有些人可能會(huì)爭(zhēng)論說這是一種不合理的行為襟诸,parseInt(..)應(yīng)當(dāng)拒絕在一個(gè)非string值上操作瓦堵。它應(yīng)該拋出一個(gè)錯(cuò)誤嗎?坦白地說歌亲,像Java那樣菇用。但是一想到JS應(yīng)當(dāng)開始在滿世界拋出錯(cuò)誤,以至于幾乎每一行代碼都需要用try..catch圍起來陷揪,我就不寒而栗惋鸥。

它應(yīng)當(dāng)返回NaN嗎?也許悍缠。但是……要是這樣呢:

parseInt( new String( "42") );

這也應(yīng)當(dāng)失敗嗎卦绣?它是一個(gè)非string值啊。如果你想讓String對(duì)象包裝器被開箱成"42"飞蚓,那么42先變成"42"滤港,以使42可以被解析回來就那么不尋常嗎?

我會(huì)爭(zhēng)論說趴拧,這種可能發(fā)生的半 明確隱含 的強(qiáng)制轉(zhuǎn)換經(jīng)辰ρ可以成為非常有用的東西。比如:

var a = {
    num: 21,
    toString: function() { return String( this.num * 2 ); }
};

parseInt( a ); // 42

事實(shí)上parseInt(..)將它的值強(qiáng)制轉(zhuǎn)換為string來實(shí)施解析是十分合理的著榴。如果你傳垃圾進(jìn)去添履,那么你就會(huì)得到垃圾,不要責(zé)備垃圾桶 —— 它只是忠實(shí)地盡自己的責(zé)任脑又。

那么暮胧,如果你傳入像Infinity(很明顯是1 / 0的結(jié)果)這樣的值,對(duì)于它的強(qiáng)制轉(zhuǎn)換來說哪種string表現(xiàn)形式最有道理呢问麸?我腦中只有兩種合理的選擇:"Infinity""∞"往衷。JS選擇了"Infinity"。我很高興它這么選口叙。

我認(rèn)為在JS中 所有的值 都有某種默認(rèn)的string表現(xiàn)形式是一件好事炼绘,這樣它們就不是我們不能調(diào)試和推理的神秘黑箱了。

現(xiàn)在妄田,關(guān)于19進(jìn)制呢俺亮?很明顯,這完全是偽命題和造作疟呐。沒有真實(shí)的JS程序使用19進(jìn)制脚曾。那太荒謬了。但是启具,讓我們?cè)僖淮畏湃芜@種荒謬本讥。在19進(jìn)制中,合法的數(shù)字字符是0 - 9a - i(大小寫無關(guān))。

那么拷沸,回到我們的parseInt( 1/0, 19 )例子色查。它實(shí)質(zhì)上是parseInt( "Infinity", 19 )。它如何解析撞芍?第一個(gè)字符是"I"秧了,在愚蠢的19進(jìn)制中是值18。第二個(gè)字符"n"不再合法的數(shù)字字符集內(nèi)序无,所以這樣的解析就禮貌地停止了验毡,就像它在"42px"中遇到"p"那樣。

結(jié)果呢帝嗡?18晶通。正如它應(yīng)該的那樣。對(duì)JS來說哟玷,并非一個(gè)錯(cuò)誤或者Infinity本身狮辽,而是將我們帶到這里的一系列的行為才是 非常重要 的,不應(yīng)當(dāng)那么簡(jiǎn)單地被丟棄碗降。

其他關(guān)于parseInt(..)行為的隘竭,令人吃驚但又十分合理的例子還包括:

parseInt( 0.000008 );       // 0   ("0" from "0.000008")
parseInt( 0.0000008 );      // 8   ("8" from "8e-7")
parseInt( false, 16 );      // 250 ("fa" from "false")
parseInt( parseInt, 16 );   // 15  ("f" from "function..")

parseInt( "0x10" );         // 16
parseInt( "103", 2 );       // 2

其實(shí)parseInt(..)在它的行為上是相當(dāng)可預(yù)見和一致的。如果你正確地使用它讼渊,你就能得到合理的結(jié)果。如果你不正確地使用它尊剔,那么你得到的瘋狂結(jié)果并不是JavaScript的錯(cuò)爪幻。

明確地:* --> Boolean

現(xiàn)在,我們來檢視從任意的非boolean值到一個(gè)boolean值的強(qiáng)制轉(zhuǎn)換须误。

正如上面的String(..)Number(..)挨稿,Boolean(..)(當(dāng)然,不帶new>┝ )是強(qiáng)制進(jìn)行ToBoolean轉(zhuǎn)換的明確方法:

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

Boolean( a ); // true
Boolean( b ); // true
Boolean( c ); // true

Boolean( d ); // false
Boolean( e ); // false

Boolean( f ); // false
Boolean( g ); // false

雖然Boolean(..)是非常明確的奶甘,但是它并不常見也不為人所慣用。

正如一元+操作符將一個(gè)值強(qiáng)制轉(zhuǎn)換為一個(gè)number(參見上面的討論)祭椰,一元的!否定操作符可以將一個(gè)值明確地強(qiáng)制轉(zhuǎn)換為一個(gè)boolean臭家。問題 是它還將值從truthy翻轉(zhuǎn)為falsy,或反之方淤。所以钉赁,大多數(shù)JS開發(fā)者使用!!雙否定操作符進(jìn)行boolean強(qiáng)制轉(zhuǎn)換,因?yàn)榈诙€(gè)!將會(huì)把它翻轉(zhuǎn)回原本的true或false:

var a = "0";
var b = [];
var c = {};

var d = "";
var e = 0;
var f = null;
var g;

!!a;    // true
!!b;    // true
!!c;    // true

!!d;    // false
!!e;    // false
!!f;    // false
!!g;    // false

沒有Boolean(..)!!的話携茂,任何這些ToBoolean強(qiáng)制轉(zhuǎn)換都將 隱含地 發(fā)生你踩,比如在一個(gè)if (..) ..語句這樣使用boolean的上下文中。但這里的目標(biāo)是,明確地強(qiáng)制一個(gè)值成為boolean來使ToBoolean強(qiáng)制轉(zhuǎn)換的意圖顯得明明白白带膜。

另一個(gè)ToBoolean強(qiáng)制轉(zhuǎn)換的用例是吩谦,如果你想在數(shù)據(jù)結(jié)構(gòu)的JSON序列化中強(qiáng)制轉(zhuǎn)換一個(gè)true/false

var a = [
    1,
    function(){ /*..*/ },
    2,
    function(){ /*..*/ }
];

JSON.stringify( a ); // "[1,null,2,null]"

JSON.stringify( a, function(key,val){
    if (typeof val == "function") {
        // 強(qiáng)制函數(shù)進(jìn)行 `ToBoolean` 轉(zhuǎn)換
        return !!val;
    }
    else {
        return val;
    }
} );
// "[1,true,2,true]"

如果你是從Java來到JavaScript的話,你可能會(huì)認(rèn)得這個(gè)慣用法:

var a = 42;

var b = a ? true : false;

? :三元操作符將會(huì)測(cè)試a的真假膝藕,然后根據(jù)這個(gè)測(cè)試的結(jié)果相應(yīng)地將truefalse賦值給b式廷。

表面上,這個(gè)慣用法看起來是一種 明確的 ToBoolean類型強(qiáng)制轉(zhuǎn)換形式束莫,因?yàn)楹苊黠@它操作的結(jié)果要么是true要么是false懒棉。

然而,這里有一個(gè)隱藏的 隱含 強(qiáng)制轉(zhuǎn)換览绿,就是表達(dá)式a不得不首先被強(qiáng)制轉(zhuǎn)換為boolean來進(jìn)行真假測(cè)試策严。我稱這種慣用法為“明確地隱含”。另外饿敲,我建議你在JavaScript中 完全避免這種慣用法妻导。它不會(huì)提供真正的好處,而且會(huì)讓事情變得更糟怀各。

對(duì)于 明確的 強(qiáng)制轉(zhuǎn)換Boolean(a)!!a是好得多的選項(xiàng)倔韭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市瓢对,隨后出現(xiàn)的幾起案子寿酌,更是在濱河造成了極大的恐慌,老刑警劉巖硕蛹,帶你破解...
    沈念sama閱讀 217,277評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件醇疼,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡法焰,警方通過查閱死者的電腦和手機(jī)秧荆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來埃仪,“玉大人乙濒,你說我怎么就攤上這事÷羊龋” “怎么了颁股?”我有些...
    開封第一講書人閱讀 163,624評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)毙玻。 經(jīng)常有香客問我豌蟋,道長(zhǎng),這世上最難降的妖魔是什么桑滩? 我笑而不...
    開封第一講書人閱讀 58,356評(píng)論 1 293
  • 正文 為了忘掉前任梧疲,我火速辦了婚禮允睹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘幌氮。我一直安慰自己缭受,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評(píng)論 6 392
  • 文/花漫 我一把揭開白布该互。 她就那樣靜靜地躺著米者,像睡著了一般。 火紅的嫁衣襯著肌膚如雪宇智。 梳的紋絲不亂的頭發(fā)上蔓搞,一...
    開封第一講書人閱讀 51,292評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音随橘,去河邊找鬼喂分。 笑死,一個(gè)胖子當(dāng)著我的面吹牛机蔗,可吹牛的內(nèi)容都是我干的蒲祈。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼萝嘁,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼梆掸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起牙言,我...
    開封第一講書人閱讀 38,992評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤酸钦,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后咱枉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體钝鸽,經(jīng)...
    沈念sama閱讀 45,429評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評(píng)論 3 334
  • 正文 我和宋清朗相戀三年庞钢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片因谎。...
    茶點(diǎn)故事閱讀 39,785評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡基括,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出财岔,到底是詐尸還是另有隱情风皿,我是刑警寧澤,帶...
    沈念sama閱讀 35,492評(píng)論 5 345
  • 正文 年R本政府宣布匠璧,位于F島的核電站桐款,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏夷恍。R本人自食惡果不足惜魔眨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧遏暴,春花似錦侄刽、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至杂彭,卻和暖如春墓毒,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背亲怠。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工所计, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人赁炎。 一個(gè)月前我還...
    沈念sama閱讀 47,891評(píng)論 2 370
  • 正文 我出身青樓醉箕,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親徙垫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子讥裤,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容