感謝社區中各位的大力支持,譯者再次奉上一點點福利:阿里云產品券,享受所有官網優惠,并抽取幸運大獎:點擊這里領取
現在我們更全面地了解了JavaScript的類型和值,我們將注意力轉向一個極具爭議的話題:強制轉換。
正如我們在第一章中提到的,關于強制轉換到底是一個有用的特性,還是一個語言設計上的缺陷(或位于兩者之間?。缇烷_始就爭論不休了。如果你讀過關于JS的其他書籍,你就會知道流行在世面上那種淹沒一切的 聲音:強制轉換是魔法,是邪惡的,令人困惑的,而且就是徹頭徹尾的壞主意。
本著這個系列叢書的總體精神,我認為你應當直面你不理解的東西并設法更全面地 搞懂它。而不是因為大家都這樣做,或是你曾經被一些怪東西咬到就逃避強制轉換。
我們的目標是全面地探索強制轉換的優點和缺點(是的,它們 有 優點!),這樣你就能在程序中對它是否合適做出明智的決定。
轉換值
將一個值從一個類型明確地轉換到另一個類型通常稱為“類型轉換(type casting)”,當這個操作隱含地完成時稱為“強制轉換(coercion)”(根據一個值如何被使用的規則來強制它變換類型)。
注意: 這可能不明顯,但是JavaScript強制轉換總是得到基本標量值的一種,比如string
,number
,或boolean
。沒有強制轉換可以得到像object
和function
這樣的復雜值。第三章講解了“封箱”,它將一個基本類型標量值包裝在它們相應的object
中,但在準確的意義上這不是真正的強制轉換。
另一種區別這些術語的常見方法是:“類型轉換(type casting/conversion)”發生在靜態類型語言的編譯時,而“類型強制轉換(type coercion)”是動態類型語言的運行時轉換。
然而,在JavaScript中,大多數人將所有這些類型的轉換都稱為 強制轉換(coercion),所以我偏好的區別方式是使用“隱含強制轉換(implicit coercion)”與“明確強制轉換(explicit coercion)”。
其中的區別應當是很明顯的:在觀察代碼時如果一個類型轉換明顯是有意為之的,那么它就是“明確強制轉換”,而如果這個類型轉換是做為其他操作的不那么明顯的副作用發生的,那么它就是“隱含強制轉換”。
例如,考慮這兩種強制轉換的方式:
var a = 42;
var b = a + ""; // 隱含強制轉換
var c = String( a ); // 明確強制轉換
對于b
來說,強制轉換是隱含地發生的,因為如果與+
操作符組合的操作數之一是一個string
值(""
),這將使+
操作成為一個string
連接(將兩個字符串加在一起),而string
連接的 一個(隱藏的)副作用 將a
中的值42
強制轉換為它的string
等價物:"42"
。
相比之下,String(..)
函數使一切相當明顯,它明確地取得a
中的值,并把它強制轉換為一個string
表現形式。
兩種方式都能達到相同的效果:從42
變成"42"
。但它們 如何 達到這種效果,才是關于JavaScript強制轉換的熱烈爭論的核心。
注意: 技術上講,這里有一些在語法形式區別之上的,行為上的微妙區別。我們將在本章稍后,“隱含:Strings <--> Numbers”一節中仔細講解。
“明確地”,“隱含地”,或“明顯地”和“隱藏的副作用”這些術語,是 相對的。
如果你確切地知道a + ""
是在做什么,并且你有意地這么做來強制轉換一個string
,你可能感覺這個操作已經足夠“明確”了。相反,如果你從沒見過String(..)
函數被用于string
強制轉換,那么對你來說它的行為可能看起來太過隱蔽而讓你感到“隱含”。
但我們是基于一個 大眾的,充分了解,但不是專家或JS規范愛好者的 開發者的觀點來討論“明確”與“隱含”的。無論你的程度如何,或是沒有在這個范疇內準確地找到自己,你都需要根據我們在這里的觀察方式,相應地調整你的角度。
記?。何覀冏约簩懘a而也只有我們自己會讀它,通常是很少見的。即便你是一個精通JS里里外外的專家,也要考慮一個經驗沒那么豐富的隊友在讀你的代碼時感受如何。對于他們和對于你來說,“明確”或“隱含”的意義相同嗎?
抽象值操作
在我們可以探究 明確 與 隱含 強制轉換之前,我們需要學習一些基本規則,是它們控制著值如何 變成 一個string
,number
,或boolean
的。ES5語言規范的第9部分用值的變形規則定義了幾種“抽象操作”(“僅供內部使用的操作”的高大上說法)。我們將特別關注于:ToString
,ToNumber
,和ToBoolean
,并稍稍關注一下ToPrimitive
。
ToString
當任何一個非string
值被強制轉換為一個string
表現形式時,這個轉換的過程是由語言規范的9.8部分的ToString
抽象操作處理的。
內建的基本類型值擁有自然的字符串化形式:null
變為"null"
,undefined
變為"undefined"
,true
變為"true"
。number
一般會以你期望的自然方式表達,但正如我們在第二章中討論的,非常小或非常大的number
將會以指數形式表達:
// `1.07`乘以`1000`,7次
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 7次乘以3位 => 21位
a.toString(); // "1.07e21"
對于普通的對象,除非你指定你自己的,默認的toString()
(可以在Object.prototype.toString()
找到)將返回 內部 [[Class]]
(見第三章),例如"[object Object]"
。
但正如早先所展示的,如果一個對象上擁有它自己的toString()
方法,而你又以一種類似string
的方式使用這個對象,那么它的toString()
將會被自動調用,而且這個調用的string
結果將被使用。
注意: 技術上講,一個對象被強制轉換為一個string
要通過ToPrimitive
抽象操作(ES5語言規范,9.1部分),但是那其中的微妙細節將會在本章稍后的ToNumber
部分中講解,所以我們在這里先跳過它。
數組擁有一個覆蓋版本的默認toString()
,將數組字符串化為它所有的值(每個都字符串化)的(字符串)連接,并用","
分割每個值。
var a = [1,2,3];
a.toString(); // "1,2,3"
重申一次,toString()
可以明確地被調用,也可以通過在一個需要string
的上下文環境中使用一個非string
來自動地被調用。
JSON字符串化
另一種看起來與ToString
密切相關的操作是,使用JSON.stringify(..)
工具將一個值序列化為一個JSON兼容的string
值。
重要的是要注意,這種字符串化與強制轉換并不完全是同一種東西。但是因為它與上面講的ToString
規則有關聯,我們將在這里稍微轉移一下話題,來講解JSON字符串化行為。
對于最簡單的值,JSON字符串化行為基本上和toString()
轉換是相同的,除了序列化的結果 總是一個string
:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (一個包含雙引號的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
任何 JSON安全 的值都可以被JSON.stringify(..)
字符串化。但是什么是 JSON安全的?任何可以用JSON表現形式合法表達的值。
考慮JSON 不 安全的值可能更容易一些。一些例子是:undefined
,function
,(ES6+)symbol
,和帶有循環引用的object
(一個對象結構中的屬性互相引用而造成了一個永不終結的循環)。對于標準的JSON結構來說這些都是非法的值,主要是因為它們不能移植到消費JSON值的其他語言中。
JSON.stringify(..)
工具在遇到undefined
,function
,和symbol
時將會自動地忽略它們。如果在一個array
中遇到這樣的值,它會被替換為null
(這樣數組的位置信息就不會改變)。如果在一個object
的屬性中遇到這樣的值,這個屬性會被簡單地剔除掉。
考慮下面的代碼:
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(..)
一個帶有循環引用的object
,就會拋出一個錯誤。
JSON字符串化有一個特殊行為,如果一個object
值定義了一個toJSON()
方法,這個方法將會被首先調用,以取得用于序列化的值。
如果你打算JSON字符串化一個可能含有非法JSON值的對象,或者如果這個對象中正好有不適于序列化的值,那么你就應當為它定義一個toJSON()
方法,返回這個object
的一個 JSON安全 版本。
例如:
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在`a`內部制造一個循環引用
o.e = a;
// 這回因循環引用而拋出一個錯誤
// JSON.stringify( a );
// 自定義一個JSON值序列化
a.toJSON = function() {
// 序列化僅包含屬性`b`
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
一個很常見的誤解是,toJSON()
應當返回一個JSON字符串化的表現形式。這可能是不正確的,除非你事實上想要字符串化string
本身(通常不會?。?code>toJSON()應當返回合適的實際普通值(無論什么類型),而JSON.stringify(..)
自己會處理字符串化。
換句話說,toJSON()
應當被翻譯為:“變為一個適用于字符串化的JSON安全的值”,不是像許多開發者錯誤認為的那樣,“變為一個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]""
在第二個調用中,我們字符串化了返回的string
而不是array
本身,這可能不是我們想要做的。
既然我們說到了JSON.stringify(..)
,那么就讓我們來討論一些不那么廣為人知,但是仍然很有用的功能吧。
JSON.stringify(..)
的第二個參數值是可選的,它稱為 替換器(replacer)。這個參數值既可以是一個array
也可以是一個function
。與toJSON()
為序列化準備一個值的方式類似,它提供一種過濾機制,指出一個object
的哪一個屬性應該或不應該被包含在序列化形式中,來自定義這個object
的遞歸序列化行為。
如果 替換器 是一個array
,那么它應當是一個string
的array
,它的每一個元素指定了允許被包含在這個object
的序列化形式中的屬性名稱。如果一個屬性不存在于這個列表中,那么它就會被跳過。
如果 替換器 是一個function
,那么它會為object
本身而被調用一次,并且為這個object
中的每個屬性都被調用一次,而且每次都被傳入兩個參數值,key 和 value。要在序列化中跳過一個 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
替換器 的情況下,第一次調用時key參數k
是undefined
(而對象a
本身會被傳入)。if
語句會 過濾掉 名稱為c
的屬性。字符串化是遞歸的,所以數組[1,2,3]
會將它的每一個值(1
,2
,和3
)都作為v
傳遞給 替換器,并將索引值(0
,1
,和2
)作為k
。
JSON.stringify(..)
還可以接收第三個可選參數值,稱為 填充符(space),在對人類友好的輸出中它被用做縮進。填充符 可以是一個正整數,用來指示每一級縮進中應當使用多少個空格字符?;蛘?,填充符 可以是一個string
,這時每一級縮進將會使用它的前十個字符。
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(..)
并不直接是一種強制轉換的形式。但是,我們在這里討論它,是由于兩個與ToString
強制轉換有關聯的行為:
-
string
,number
,boolean
,和null
值在JSON字符串化時,與它們通過ToString
抽象操作的規則強制轉換為string
值的方式基本上是相同的。 - 如果傳遞一個
object
值給JSON.stringify(..)
,而這個object
上擁有一個toJSON()
方法,那么在字符串化之前,toJSON()
就會被自動調用來將這個值(某種意義上)“強制轉換”為 JSON安全 的。
ToNumber
如果任何非number
值,以一種要求它是number
的方式被使用,比如數學操作,就會發生ES5語言規范在9.3部分定義的ToNumber
抽象操作。
例如,true
變為1
而false
變為0
。undefined
變為NaN
,而(奇怪的是)null
變為0
。
對于一個string
值來說,ToNumber
工作起來很大程度上與數字字面量的規則/語法很相似(見第三章)。如果它失敗了,結果將是NaN
(而不是number
字面量中會出現的語法錯誤)。一個不同之處的例子是,在這個操作中0
前綴的八進制數不會被作為八進制數來處理(而僅作為普通的十進制小數),雖然這樣的八進制數作為number
字面量是合法的。
注意: number
字面量文法與用于string
值的ToNumber
間的區別極其微妙,在這里就不進一步講解了。更多的信息可以參考ES語言規范的9.3.1部分。
對象(以及數組)將會首先被轉換為它們的基本類型值的等價物,而后這個結果值(如果它還不是一個number
基本類型)會根據剛才提到的ToNumber
規則被強制轉換為一個number
。
為了轉換為基本類型值的等價物,ToPrimitive
抽象操作(ES5語言規范,9.1部分)將會查詢這個值(使用內部的DefaultValue
操作 —— ES5語言規范,8.12.8部分),看它有沒有valueOf()
方法。如果valueOf()
可用并且它返回一個基本類型值,那么 這個 值就將用于強制轉換。如果不是這樣,但toString()
可用,那么就由它來提供用于強制轉換的值。
如果這兩種操作都沒提供一個基本類型值,就會拋出一個TypeError
。
在ES5中,你可以創建這樣一個不可強制轉換的對象 —— 沒有valueOf()
和toString()
—— 如果它的[[Prototype]]
的值為null
,這通常是通過Object.create(null)
來創建的。關于[[Prototype]]
的詳細信息參見本系列的 this與對象原型。
注意: 我們會在本章稍后講解如何強制轉換至number
,但對于下面的代碼段,想象Number(..)
函數就是那樣做的。
考慮如下代碼:
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
如何動作。世面上關于這個話題有 許多的困惑和誤解,所以集中注意力!
首先而且最重要的是,JS實際上擁有true
和false
關鍵字,而且它們的行為正如你所期望的boolean
值一樣。一個常見的誤解是,值1
和0
與true
/false
是相同的。雖然這可能在其他語言中是成立的,但在JS中number
就是number
,而boolean
就是boolean
。你可以將1
強制轉換為true
(或反之),或將0
強制轉換為false
(或反之)。但它們不是相同的。
Falsy值
但這還不是故事的結尾。我們需要討論一下,除了這兩個boolean
值以外,當你把其他值強制轉換為它們的boolean
等價物時如何動作。
所有的JavaScript值都可以被劃分進兩個類別:
- 如果被強制轉換為
boolean
,將成為false
的值 - 其它的一切值(很明顯將變為
true
)
我不是在出洋相。JS語言規范給那些在強制轉換為boolean
值時將會變為false
的值定義了一個明確的,小范圍的列表。
我們如何才能知道這個列表中的值是什么?在ES5語言規范中,9.2部分定義了一個ToBoolean
抽象操作,它講述了對所有可能的值而言,當你試著強制轉換它們為boolean時究竟會發生什么。
從這個表格中,我們得到了下面所謂的“falsy”值列表:
undefined
null
false
-
+0
,-0
, andNaN
""
就是這些。如果一個值在這個列表中,它就是一個“falsy”值,而且當你在它上面進行boolean
強制轉換時它會轉換為false
。
通過邏輯上的推論,如果一個值 不 在這個列表中,那么它一定在 另一個列表 中,也就是我們稱為“truthy”值的列表。但是JS沒有真正定義一個“truthy”列表。它給出了一些例子,比如它說所有的對象都是truthy,但是語言規范大致上暗示著:任何沒有明確地存在于falsy列表中的東西,都是truthy。
Falsy對象
等一下,這一節的標題聽起來簡直是矛盾的。我 剛剛才說過 語言規范將所有對象稱為truthy,對吧?應該沒有“falsy對象”這樣的東西。
這會是什么意思呢?
它可能誘使你認為它意味著一個包裝了falsy值(比如""
,0
或false
)的對象包裝器(見第三章)。但別掉到這個 陷阱 中。
注意: 這個可能是一個語言規范的微妙笑話。
考慮下面的代碼:
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
我們知道這三個值都是包裝了明顯是falsy值的對象(見第三章)。但這些對象是作為true
還是作為false
動作呢?這很容易回答:
var d = Boolean( a && b && c );
d; // true
所以,三個都作為true
動作,這是唯一能使d
得到true
的方法。
提示: 注意包在a && b && c
表達式外面的Boolean( .. )
—— 你可能想知道為什么它在這兒。我們會在本章稍后回到這個話題,所以先做個心理準備。為了先睹為快,你可以自己試試如果沒有Boolean( .. )
調用而只有d = a && b && c
時d
是什么。
那么,如果“falsy對象” 不是包裝著falsy值的對象,它們是什么鬼東西?
刁鉆的地方在于,它們可以出現在你的JS程序中,但它們實際上不是JavaScript本身的一部分。
什么!?
有些特定的情況,在普通的JS語義之上,瀏覽器已經創建了它們自己的某種 外來 值的行為,也就是這種“falsy對象”的想法。
一個“falsy對象”看起來和動起來都像一個普通對象(屬性,等等)的值,但是當你強制轉換它為一個boolean
時,它會變為一個false
值。
為什么!?
最著名的例子是document.all
:一個 由DOM(不是JS引擎本身) 給你的JS程序提供的類數組(對象),它向你的JS程序暴露你頁面上的元素。它 曾經 像一個普通對象那樣動作 —— 是一個truthy。但不再是了。
document.all
本身從來就不是“標準的”,而且從很早以前就被廢棄/拋棄了。
“那他們就不能刪掉它嗎?” 對不起,想得不錯。但愿它們能。但是世面上有太多的遺產JS代碼庫依賴于它。
那么,為什么使它像falsy一樣動作?因為從document.all
到boolean
的強制轉換(比如在if
語句中)幾乎總是用來檢測老的,非標準的IE。
IE從很早以前就開始順應規范了,而且在許多情況下它在推動web向前發展的作用和其他瀏覽器一樣多,甚至更多。但是所有那些老舊的if (document.all) { /* it's IE */ }
代碼依然留在世面上,而且大多數可能永遠都不會消失。所有這些遺產代碼依然假設它們運行在那些給IE用戶帶來差勁兒的瀏覽體驗的,幾十年前的老IE上,
所以,我們不能完全移除document.all
,但是IE不再想讓if (document.all) { .. }
代碼繼續工作了,這樣現代IE的用戶就能得到新的,符合標準的代碼邏輯。
“我們應當怎么做?” “我知道了!讓我們黑進JS的類型系統并假裝document.all
是falsy!”
呃。這很爛。這是一個大多數JS開發者們都不理解的瘋狂的坑。但是其它的替代方案(對上面兩敗俱傷的問題什么都不做)還要爛得 多那么一點點。
所以……這就是我們得到的:由瀏覽器給JavaScript添加的瘋狂,非標準的“falsy對象”。耶!
Truthy值
回到truthy列表。到底什么是truthy值?記住:如果一個值不在falsy列表中,它就是truthy。
考慮下面代碼:
var a = "false";
var b = "0";
var c = "''";
var d = Boolean( a && b && c );
d;
你期望這里的d
是什么值?它要么是true
要么是false
。
它是true
。為什么?因為盡管這些string
值的內容看起來是falsy值,但是string
值本身都是truthy,而這是因為在falsy列表中""
是唯一的string
值。
那么這些呢?
var a = []; // 空數組 -- truthy 還是 falsy?
var b = {}; // 空對象 -- truthy 還是 falsy?
var c = function(){}; // 空函數 -- truthy 還是 falsy?
var d = Boolean( a && b && c );
d;
是的,你猜到了,這里的d
依然是true
。為什么?和前面的原因一樣。盡管它們看起來像,但是[]
,{}
,和function(){}
不在 falsy列表中,因此它們是truthy值。
換句話說,truthy列表是無限長的。不可能制成一個這樣的列表。你只能制造一個falsy列表并查詢它。
花五分鐘,把falsy列表寫在便利貼上,然后粘在你的電腦顯示器上,或者如果你愿意就記住它。不管哪種方法,你都可以在自己需要的時候通過簡單地查詢一個值是否在falsy列表中,來構建一個虛擬的truthy列表。
truthy和falsy的重要性在于,理解如果一個值在被(明確地或隱含地)強制轉換為boolean
值的話,它將如何動作?,F在你的大腦中有了這兩個列表,我們可以深入強制轉換的例子本身了。
明確的強制轉換
明確的 強制轉換指的是明顯且明確的類型轉換。對于大多數開發者來說,有很多類型轉換的用法可以清楚地歸類于這種 明確的 強制轉換。
我們在這里的目標是,在我們的代碼中指明一些模式,在這些模式中我們可以清楚明白地將一個值從一種類型轉換至另一種類型,以確保不給未來將讀到這段代碼的開發者留下任何坑。我們越明確,后來的人就越容易讀懂我們的代碼,也不必費太多的力氣去理解我們的意圖。
關于 明確的 強制轉換可能很難找到什么主要的不同意見,因為它與被廣泛接受的靜態類型語言中的類型轉換的工作方式非常接近。因此,我們理所當然地認為(暫且) 明確的 強制轉換可以被認同為不是邪惡的,或沒有爭議的。雖然我們稍后會回到這個話題。
明確地:Strings <--> Numbers
我們將從最簡單,也許是最常見強制轉換操作開始:將值在string
和number
表現形式之間進行強制轉換。
為了在string
和number
之間進行強制轉換,我們使用內建的String(..)
和Number(..)
函數(我們在第三章中所指的“原生構造器”),但 非常重要的是,我們不在它們前面使用new
關鍵字。這樣,我們就不是在創建對象包裝器。
取而代之的是,我們實際上在兩種類型之間進行 明確地強制轉換:
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14
String(..)
使用早先討論的ToString
操作的規則,將任意其它的值強制轉換為一個基本類型的string
值。Number(..)
使用早先討論過的ToNumber
操作的規則,將任意其他的值強制轉換為一個基本類型的number
值。
我稱此為 明確的 強制轉換是因為,一般對于大多數開發者來說這是十分明顯的:這些操作的最終結果是適當的類型轉換。
實際上,這種用法看起來與其他的靜態類型語言中的用法非常相像。
舉個例子,在C/C++中,你既可以說(int)x
也可以說int(x)
,而且它們都將x
中的值轉換為一個整數。兩種形式都是合法的,但是許多人偏向于后者,它看起來有點兒像一個函數調用。在JavaScript中,當你說Number(x)
時,它看起來極其相似。在JS中它實際上是一個函數調用這個事實重要嗎?并非如此。
除了String(..)
和Number(..)
,還有其他的方法可以把這些值在string
和number
之間進行“明確地”轉換:
var a = 42;
var b = a.toString();
var c = "3.14";
var d = +c;
b; // "42"
d; // 3.14
調用a.toString()
在表面上是明確的(“toString”意味著“變成一個字符串”是很明白的),但是這里有一些藏起來的隱含性。toString()
不能在像42
這樣的 基本類型 值上調用。所以JS會自動地將42
“封箱”在一個對象包裝器中(見第三章),這樣toString()
就可以針對這個對象調用。換句話講,你可能會叫它“明確的隱含”。
這里的+c
是+
操作符的 一元操作符(操作符只有一個操作數)形式。取代進行數學加法(或字符串連接 —— 見下面的討論)的是,一元的+
明確地將它的操作數(c
)強制轉換為一個number
值。
+c
是 明確的 強制轉換嗎?這要看你的經驗和角度。如果你知道(現在你知道了!)一元+
明確地意味著number
強制轉換,那么它就是相當明確和明顯的。但是,如果你以前從沒見過它,那么它看起來就極其困惑,晦澀,帶有隱含的副作用,等等。
注意: 在開源的JS社區中一般被接受的觀點是,一元+
是一個 明確的 強制轉換形式。
即使你真的喜歡+c
這種形式,它絕對會在有的地方看起來非常令人困惑??紤]下面的代碼:
var c = "3.14";
var d = 5+ +c;
d; // 8.14
一元-
操作符也像+
一樣進行強制轉換,但它還會翻轉數字的符號。但是你不能放兩個減號--
來使符號翻轉回來,因為那將被解釋為遞減操作符。取代它的是,你需要這么做:- -"3.14"
,在兩個減號之間加入空格,這將會使強制轉換的結果為3.14
。
你可能會想到所有種類的可怕組合 —— 一個二元操作符挨著另一個操作符的一元形式。這里有另一個瘋狂的例子:
1 + - + + + - + 1; // 2
當一個一元+
(或-
)緊鄰其他操作符時,你應當強烈地考慮避免使用它。雖然上面的代碼可以工作,但幾乎全世界都認為它是一個壞主意。即使是d = +c
(或者d =+ c
?。┒继菀着cd += c
像混淆了,而后者完全是不同的東西!
注意: 一元+
的另一個極端使人困惑的地方是,被用于緊挨著另一個將要作為++
遞增操作符和--
遞減操作符的操作數。例如:a +++b
,a + ++b
,和a + + +b
。更多關于++
的信息,參見第五章的“表達式副作用”。
記住,我們正努力變得明確并 減少 困惑,不是把事情弄得更糟!
從Date
到number
另一個一元+
操作符的常見用法是將一個Date
對象強制轉換為一個number
,其結果是這個日期/時間值的unix時間戳(從世界協調時間的1970年1月1日0點開始計算,經過的毫秒數)表現形式:
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000
這種習慣性用法經常用于取得當前的 現在 時刻的時間戳,比如:
var timestamp = +new Date();
注意: 一些開發者知道一個JavaScript中的特別的語法“技巧”,就是在構造器調用(一個帶有new
的函數調用)中如果沒有參數值要傳遞的話,()
是 可選的。所以你可能遇到var timestamp = +new Date;
形式。然而,不是所有的開發者都同意忽略()
可以增強可讀性,因為它是一種不尋常的語法特例,只能適用于new fn()
調用形式,而不能用于普通的fn()
調用形式。
但強制轉換不是從Date
對象中取得時間戳的唯一方法。一個不使用強制轉換的方式可能更好,因為它更加明確:
var timestamp = new Date().getTime();
// var timestamp = (new Date()).getTime();
// var timestamp = (new Date).getTime();
但是一個 更更好的 不使用強制轉換的選擇是使用ES5加入的Date.now()
靜態函數:
var timestamp = Date.now();
而且如果你想要為老版本的瀏覽器填補Date.now()
的話,也十分簡單:
if (!Date.now) {
Date.now = function() {
return +new Date();
};
}
我推薦跳過與日期有關的強制轉換形式。使用Date.now()
來取得當前 現在 的時間戳,而使用new Date( .. ).getTime()
來取得一個需要你指定的 非現在 日期/時間的時間戳。
奇異的~
一個經常被忽視并通常讓人糊涂的JS強制操作符是波浪線~
操作符(也叫“按位取反”,“比特非”)。許多理解它在做什么的人也總是想要避開它。但是為了堅持我們在本書和本系列中的精神,讓我們深入并找出~
是否有一些對我們有用的東西。
在第二章的“32位(有符號)整數”一節,我們講解了在JS中位操作符是如何僅為32位操作定義的,這意味著我們強制它們的操作數遵循32位值的表現形式。這個規則如何發生是由ToInt32
抽象操作(ES5語言規范,9.5部分)控制的。
ToInt32
首先進行ToNumber
強制轉換,這就是說如果值是"123"
,它在ToInt32
規則實施之前會首先變成123
。
雖然它本身沒有 技術上進行 強制轉換(因為類型沒有改變),但對一些特定的特殊number
值使用位操作符(比如|
或~
)會產生一種強制轉換效果,這種效果的結果是一個不同的number
值。
舉例來說,讓我們首先考慮慣用的空操作0 | x
(在第二種章有展示)中使用的|
“比特或”操作符,它實質上僅僅進行ToInt32
轉換:
0 | -0; // 0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0
這些特殊的數字是不可用32位表現的(因為它們源自64位的IEEE 754標準 —— 見第二章),所以ToInt32
將這些值的結果指定為0
。
有爭議的是,0 | __
是否是一種ToInt32
強制轉換操作的 明確的 形式,還是更傾向于 隱含。從語言規范的角度來說,毫無疑問是 明確的,但是如果你沒有在這樣的層次上理解位操作,它就可能看起來有點像 隱含的 魔法。不管怎樣,為了與本章中其他的斷言保持一致,我們稱它為 明確的。
那么,讓我們把注意力轉回~
。~
操作符首先將值“強制轉換”為一個32位number
值,然后實施按位取反(翻轉每一個比特位)。
注意: 這與!
不僅強制轉換它的值為boolean
而且還翻轉它的每一位很相似(見后面關于“一元!
”的討論)。
但是……什么!?為什么我們要關心被翻轉的比特位?這是一些相當特殊的,微妙的東西。JS開發者需要推理個別比特位是十分少見的。
另一種考慮~
定義的方法是,~
源自學校中的計算機科學/離散數學:~
進行二進制取補操作。太好了,謝謝,我完全明白了!
我們再試一次:~x
大致與-(x+1)
相同。這很奇怪,但是稍微容易推理一些。所以:
~42; // -(42+1) ==> -43
你可能還在想~
這個鬼東西到底和什么有關,或者對于強制轉換的討論它究竟有什么要緊。讓我們快速進入要點。
考慮一下-(x+1)
。通過進行這個操作,能夠產生結果0
(或者從技術上說-0
?。┑奈ㄒ坏闹凳鞘裁矗?code>-1。換句話說,~
用于一個范圍的number
值時,將會為輸入值-1
產生一個falsy(很容易強制轉換為false
)的0
,而為任意其他的輸入產生truthy的number
。
為什么這要緊?
-1
通常稱為一個“哨兵值”,它基本上意味著一個在同類型值(number
)的更大的集合中被賦予了任意的語義。在C語言中許多函數使用哨兵值-1
,它們返回>= 0
的值表示“成功”,返回-1
表示“失敗”。
JavaScript在定義string
操作indexOf(..)
時采納了這種先例,它搜索一個子字符串,如果找到就返回它從0開始計算的索引位置,沒有找到的話就返回-1
。
這樣的情況很常見:不僅僅將indexOf(..)
作為取得位置的操作,而且作為檢查一個子字符串存在/不存在于另一個string
中的boolean
值。這就是開發者們通常如何進行這樣的檢查:
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
有些惡心。它基本上是一種“抽象泄漏”,這里它將底層的實現行為 —— 使用哨兵值-1
表示“失敗” —— 泄漏到我的代碼中。我倒是樂意隱藏這樣的細節。
現在,我們終于看到為什~
可以幫到我們了!將~
和indexOf()
一起使用可以將值“強制轉換”(實際上只是變形)為 可以適當地強制轉換為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(..)
的返回值并將它變形:對于“失敗”的-1
我們得到falsy的0
,而其他的值都是truthy。
注意: ~
的假想算法-(x+1)
暗示著~-1
是-0
,但是實際上它產生0
,因為底層的操作其實是按位的,不是數學操作。
技術上將,if (~a.indexOf(..))
仍然依靠 隱含的 強制轉換將它的結果0
變為false
或非零變為true
。但總的來說,對我而言~
更像一種 明確的 強制轉換機制,只要你知道在這種慣用法中它的意圖是什么。
我感覺這樣的代碼要比前面凌亂的>= 0
/ == -1
更干凈。
截斷比特位
在你遇到的代碼中,還有一個地方可能出現~
:一些開發者使用雙波浪線~~
來截斷一個number
的小數部分(也就是,將它“強制轉換”為一個“整數”)。這通常(雖然是錯誤的)被說成與調用Math.floor(..)
的結果相同。
~ ~
的工作方式是,第一個~
實施ToInt32
“強制轉換”并進行按位取反,然后第二個~
進行另一次按位取反,將每一個比特位都翻轉回原來的狀態。于是最終的結果就是ToInt32
“強制轉換”(也叫截斷)。
注意: ~~
的按位雙翻轉,與雙否定!!
的行為非常相似,它將在稍后的“明確地:* --> Boolean”一節中講解。
然而,~~
需要一些注意/澄清。首先,它僅在32位值上可以可靠地工作。但更重要的是,它在負數上工作的方式與Math.floor(..)
不同!
Math.floor( -49.6 ); // -50
~~-49.6; // -49
把Math.floor(..)
的不同放在一邊,~~x
可以將值截斷為一個(32位)整數。但是x | 0
也可以,而且看起來還(稍微)省事兒 一些。
那么,為什么你可能會選擇~~x
而不是x | 0
?操作符優先權(見第五章):
~~1E20 / 10; // 166199296
1E20 | 0 / 10; // 1661992960
(1E20 | 0) / 10; // 166199296
正如這里給出的其他建議一樣,僅在讀/寫這樣的代碼的每一個人都知道這些操作符如何工作的情況下,才將~
和~~
作為“強制轉換”和將值變形的明確機制。
明確地:解析數字字符串
將一個string
強制轉換為一個number
的類似結果,可以通過從string
的字符內容中解析(parsing)出一個number
得到。然而在這種解析和我們上面講解的類型轉換之間存在著區別。
考慮下面的代碼:
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
從一個字符串中解析出一個數字是 容忍 非數字字符的 —— 從左到右,如果遇到非數字字符就停止解析 —— 而強制轉換是 不容忍 并且會失敗而得出值NaN
。
解析不應當被視為強制轉換的替代品。這兩種任務雖然相似,但是有著不同的目的。當你不知道/不關心右手邊可能有什么其他的非數字字符時,你可以將一個string
作為number
解析。當只有數字才是可接受的值,而且像"42px"
這樣的東西作為數字應當被排除時,就強制轉換一個string
(變為一個number
)。
提示: parseInt(..)
有一個孿生兄弟,parseFloat(..)
,它(聽起來)從一個字符串中拉出一個浮點數。
不要忘了parseInt(..)
工作在string
值上。向parseInt(..)
傳遞一個number
絕對沒有任何意義。傳遞其他任何類型也都沒有意義,比如true
, function(){..}
或[1,2,3]
。
如果你傳入一個非string
,你所傳入的值首先將自動地被強制轉換為一個string
(見早先的“ToString
”),這很明顯是一種隱藏的 隱含 強制轉換。在你的程序中依賴這樣的行為真的是一個壞主意,所以永遠也不要將parseInt(..)
與非string
值一起使用。
在ES5之前,parseInt(..)
還存在另外一個坑,這曾是許多JS程序的bug的根源。如果你不傳遞第二個參數來指定使用哪種進制(也叫基數)來翻譯數字的string
內容,parseInt(..)
將會根據開頭的字符進行猜測。
如果開頭的兩個字符是"0x"
或"0X"
,那么猜測(根據慣例)將是你想要將這個string
翻譯為一個16進制number
。否則,如果第一個字符是"0"
,那么猜測(也是根據慣例)將是你想要將這個string
翻譯成8進制number
。
16進制的string
(以0x
或0X
開頭)沒那么容易搞混。但是事實證明8進制數字的猜測過于常見了。比如:
var hour = parseInt( selectedHour.value );
var minute = parseInt( selectedMinute.value );
console.log( "The time you selected was: " + hour + ":" + minute);
看起來無害,對吧?試著在小時上選擇08
在分鐘上選擇09
。你會得到0:0
。為什么?因為8
和9
都不是合法的8進制數。
ES5之前的修改很簡單,但是很容易忘:總是在第二個參數值上傳遞10
。這完全是安全的:
var hour = parseInt( selectedHour.value, 10 );
var minute = parseInt( selectedMiniute.value, 10 );
在ES5中,parseInt(..)
不再猜測八進制數了。除非你指定,否則它會假定為10進制(或者為"0x"
前綴猜測16進制數)。這好多了。只是要小心,如果你的代碼不得不運行在前ES5環境中,你仍然需要為基數傳遞10
。
解析非字符串
幾年以前有一個挖苦JS的玩笑,使一個關于parseInt(..)
行為的一個臭名昭著的例子備受關注,它取笑JS的這個行為:
parseInt( 1/0, 19 ); // 18
這里面設想(但完全不合法)的斷言是,“如果我傳入一個無限大,并從中解析出一個整數的話,我應該得到一個無限大,不是18”。沒錯,JS一定是瘋了才得出這個結果,對吧?
雖然這是個明顯故意造成的,不真實的例子,但是讓我們放縱這種瘋狂一小會兒,來檢視一下JS是否真的那么瘋狂。
首先,這其中最明顯的原罪是將一個非string
傳入了parseInt(..)
。這是不對的。這么做是自找麻煩。但就算你這么做了,JS也會禮貌地將你傳入的東西強制轉換為它可以解析的string
。
有些人可能會爭論說這是一種不合理的行為,parseInt(..)
應當拒絕在一個非string
值上操作。它應該拋出一個錯誤嗎?坦白地說,像Java那樣。但是一想到JS應當開始在滿世界拋出錯誤,以至于幾乎每一行代碼都需要用try..catch
圍起來,我就不寒而栗。
它應當返回NaN
嗎?也許。但是……要是這樣呢:
parseInt( new String( "42") );
這也應當失敗嗎?它是一個非string
值啊。如果你想讓String
對象包裝器被開箱成"42"
,那么42
先變成"42"
,以使42
可以被解析回來就那么不尋常嗎?
我會爭論說,這種可能發生的半 明確 半 隱含 的強制轉換經常可以成為非常有用的東西。比如:
var a = {
num: 21,
toString: function() { return String( this.num * 2 ); }
};
parseInt( a ); // 42
事實上parseInt(..)
將它的值強制轉換為string
來實施解析是十分合理的。如果你傳垃圾進去,那么你就會得到垃圾,不要責備垃圾桶 —— 它只是忠實地盡自己的責任。
那么,如果你傳入像Infinity
(很明顯是1 / 0
的結果)這樣的值,對于它的強制轉換來說哪種string
表現形式最有道理呢?我腦中只有兩種合理的選擇:"Infinity"
和"∞"
。JS選擇了"Infinity"
。我很高興它這么選。
我認為在JS中 所有的值 都有某種默認的string
表現形式是一件好事,這樣它們就不是我們不能調試和推理的神秘黑箱了。
現在,關于19進制呢?很明顯,這完全是偽命題和造作。沒有真實的JS程序使用19進制。那太荒謬了。但是,讓我們再一次放任這種荒謬。在19進制中,合法的數字字符是0
- 9
和a
- i
(大小寫無關)。
那么,回到我們的parseInt( 1/0, 19 )
例子。它實質上是parseInt( "Infinity", 19 )
。它如何解析?第一個字符是"I"
,在愚蠢的19進制中是值18
。第二個字符"n"
不再合法的數字字符集內,所以這樣的解析就禮貌地停止了,就像它在"42px"
中遇到"p"
那樣。
結果呢?18
。正如它應該的那樣。對JS來說,并非一個錯誤或者Infinity
本身,而是將我們帶到這里的一系列的行為才是 非常重要 的,不應當那么簡單地被丟棄。
其他關于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
其實parseInt(..)
在它的行為上是相當可預見和一致的。如果你正確地使用它,你就能得到合理的結果。如果你不正確地使用它,那么你得到的瘋狂結果并不是JavaScript的錯。
明確地:* --> Boolean
現在,我們來檢視從任意的非boolean
值到一個boolean
值的強制轉換。
正如上面的String(..)
和Number(..)
,Boolean(..)
(當然,不帶new
!)是強制進行ToBoolean
轉換的明確方法:
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(..)
是非常明確的,但是它并不常見也不為人所慣用。
正如一元+
操作符將一個值強制轉換為一個number
(參見上面的討論),一元的!
否定操作符可以將一個值明確地強制轉換為一個boolean
。問題 是它還將值從truthy翻轉為falsy,或反之。所以,大多數JS開發者使用!!
雙否定操作符進行boolean
強制轉換,因為第二個!
將會把它翻轉回原本的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
強制轉換都將 隱含地 發生,比如在一個if (..) ..
語句這樣使用boolean
的上下文中。但這里的目標是,明確地強制一個值成為boolean
來使ToBoolean
強制轉換的意圖顯得明明白白。
另一個ToBoolean
強制轉換的用例是,如果你想在數據結構的JSON序列化中強制轉換一個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") {
// 強制函數進行 `ToBoolean` 轉換
return !!val;
}
else {
return val;
}
} );
// "[1,true,2,true]"
如果你是從Java來到JavaScript的話,你可能會認得這個慣用法:
var a = 42;
var b = a ? true : false;
? :
三元操作符將會測試a
的真假,然后根據這個測試的結果相應地將true
或false
賦值給b
。
表面上,這個慣用法看起來是一種 明確的 ToBoolean
類型強制轉換形式,因為很明顯它操作的結果要么是true
要么是false
。
然而,這里有一個隱藏的 隱含 強制轉換,就是表達式a
不得不首先被強制轉換為boolean
來進行真假測試。我稱這種慣用法為“明確地隱含”。另外,我建議你在JavaScript中 完全避免這種慣用法。它不會提供真正的好處,而且會讓事情變得更糟。
對于 明確的 強制轉換Boolean(a)
和!!a
是好得多的選項。
隱含的強制轉換
隱含的 強制轉換是指這樣的類型轉換:它們是隱藏的,由于其他的動作隱含地發生的不明顯的副作用。換句話說,任何(對你)不明顯的類型轉換都是 隱含的強制轉換。
雖然 明確的 強制轉換的目的很明白,但是這可能 太過 明顯 —— 隱含的 強制轉換擁有相反的目的:使代碼更難理解。
從表面上來看,我相信這就是許多關于強制轉換的憤怒的源頭。絕大多數關于“JavaScript強制轉換”的抱怨實際上都指向了(不管他們是否理解它) 隱含的 強制轉換。
注意: Douglas Crockford,"JavaScript: The Good Parts" 的作者,在許多會議和他的作品中聲稱應當避免JavaScript強制轉換。但看起來他的意思是 隱含的 強制轉換是不好的(以他的意見)。然而,如果你讀他自己的代碼的話,你會發現相當多的強制轉換的例子,明確 和 隱含 都有!事實上,他的擔憂主要在于==
操作,但正如你將在本章中看到的,那只是強制轉換機制的一部分。
那么,隱含強制轉換 是邪惡的嗎?它很危險嗎?它是JavaScript設計上的缺陷嗎?我們應該盡一切力量避免它嗎?
我打賭大多數讀者都傾向于踴躍地歡呼,“是的!”
別那么著急。聽我把話說完。
讓我們在 隱含的 強制轉換是什么,和可以是什么這個問題上采取一個不同的角度,而不是僅僅說它是“好的明確強制轉換的反面”。這太過狹隘,而且忽視了一個重要的微妙細節。
讓我們將 隱含的 強制轉換的目的定義為:減少搞亂我們代碼的繁冗,模板代碼,和/或不必要的實現細節,不使它們的噪音掩蓋更重要的意圖。
用于簡化的隱含
在我們進入JavaScript以前,我建議使用某個理論上是強類型的語言的假想代碼來說明一下:
SomeType x = SomeType( AnotherType( y ) )
在這個例子中,我在y
中有一些任意類型的值,想把它轉換為SomeType
類型。問題是,這種語言不能從當前y
的類型直接走到SomeType
。它需要一個中間步驟,它首先轉換為AnotherType
,然后從AnotherType
轉換到SomeType
。
現在,要是這種語言(或者你可用這種語言創建自己的定義)允許你這么說呢:
SomeType x = SomeType( y )
難道一般來說你不會同意我們簡化了這里的類型轉換,降低了中間轉換步驟的無謂的“噪音”嗎?我的意思是,在這段代碼的這一點上,能看到并處理y
先變為AnotherType
然后再變為SomeType
的事實,真的 是很重要的一件事嗎?
有些人可能會爭辯,至少在某些環境下,是的。但我想我可以做出相同的爭辯說,在許多其他的環境下,不管是通過語言本身的還是我們自己的抽象,這樣的簡化通過抽象或隱藏這些細節 確實增強了代碼的可讀性。
毫無疑問,在幕后的某些地方,那個中間的步驟依然是發生的。但如果這樣的細節在視野中隱藏起來,我們就可以將使y
變為類型SomeType
作為一個泛化操作來推理,并隱藏混亂的細節。
雖然不是一個完美的類比,我要在本章剩余部分爭論的是,JS的 隱含的 強制轉換可以被認為是給你的代碼提供了一個類似的輔助。
但是,很重要的是,這不是一個無邊界的,絕對的論斷。絕對有許多 邪惡的東西 潛伏在 隱含 強制轉換周圍,它們對你的代碼造成的損害要比任何潛在的可讀性改善厲害的多。很清楚,我們不得不學習如何避免這樣的結構,使我們不會用各種bug來毒害我們的代碼。
許多開發者相信,如果一個機制可以做某些有用的事兒 A,但也可以被濫用或誤用來做某些可怕的事兒 Z,那么我們就應當將這種機制整個兒扔掉,僅僅是為了安全。
我對你的鼓勵是:不要安心于此。不要“把孩子跟洗澡水一起潑出去”。不要因為你只見到過它的“壞的一面”就假設 隱含 強制轉換都是壞的。我認為這里有“好的一面”,而我想要幫助和啟發你們更多的人找到并接納它們!
隱含地:Strings <--> Numbers
在本章的早先,我們探索了string
和number
值之間的 明確 強制轉換?,F在,讓我們使用 隱含 強制轉換的方式探索相同的任務。但在我們開始之前,我們不得不檢視一些將會 隱含地 發生強制轉換的操作的微妙之處。
為了服務于number
的相加和string
的連接兩個目的,+
操作符被重載了。那么JS如何知道你想用的是哪一種操作呢?考慮下面的代碼:
var a = "42";
var b = "0";
var c = 42;
var d = 0;
a + b; // "420"
c + d; // 42
是什么不同導致了"420"
和42
?一個常見的誤解是,這個不同之處在于操作數之一或兩者是否是一個string
,這意味著+
將假設string
連接。雖然這有一部分是對的,但實際情況要更復雜。
考慮如下代碼:
var a = [1,2];
var b = [3,4];
a + b; // "1,23,4"
兩個操作數都不是string
,但很明顯它們都被強制轉換為string
然后啟動了string
連接。那么到底發生了什么?
(警告: 語言規范式的深度細節就要來了,如果這會嚇到你就跳過下面兩段?。?/p>
根據ES5語言規范的11.6.1部分,+
的算法是(當一個操作數是object
值時),如果兩個操作數之一已經是一個string
,或者下列步驟產生一個string
表達形式,+
將會進行連接。所以,當+
的兩個操作數之一收到一個object
(包括array
)時,它首先在這個值上調用ToPrimitive
抽象操作(9.1部分),而它會帶著number
的上下文環境提示來調用[[DefaultValue]]
算法(8.12.8部分)。
如果你仔細觀察,你會發現這個操作現在和ToNumber
抽象操作處理object
的過程是一樣的(參見早先的“ToNumber
”一節)。在array
上的valueOf()
操作將會在產生一個簡單基本類型時失敗,于是它退回到一個toString()
表現形式。兩個array
因此分別變成了"1,2"
和"3,4"
?,F在,+
就如你通常期望的那樣連接這兩個string
:"1,23,4"
。
讓我們把這些亂七八糟的細節放在一邊,回到一個早前的,簡化的解釋:如果+
的兩個操作數之一是一個string
(或在上面的步驟中成為一個string
),那么操作就會是string
連接。否則,它總是數字加法。
注意: 關于強制轉換,一個經常被引用的坑是[] + {}
和{} + []
,這兩個表達式的結果分別是"[object Object]"
和0
。雖然對此有更多的東西,但是我們將在第五章的“Block”中講解這其中的細節。
這對 隱含 強制轉換意味著什么?
你可以簡單地通過將number
和空string``""
“相加”來把一個number
強制轉換為一個string
:
var a = 42;
var b = a + "";
b; // "42"
提示: 使用+
操作符的數字加法是可交換的,這意味著2 + 3
與3 + 2
是相同的。使用+
的字符串連接很明顯通常不是可交換的,但是 對于""
的特定情況,它實質上是可交換的,因為a + ""
和"" + a
會產生相同的結果。
使用一個+ ""
操作將number
(隱含地)強制轉換為string
是極其常見/慣用的。事實上,有趣的是,一些在口頭上批評 隱含 強制轉換得最嚴厲的人仍然在他們自己的代碼中使用這種方式,而不是使用它的 明確的 替代形式。
在 隱含 強制轉換的有用形式中,我認為這是一個很棒的例子,盡管這種機制那么頻繁地被人詬?。?/p>
將a + ""
這種 隱含的 強制轉換與我們早先的String(a)
明確的 強制轉換的例子相比較,有一個另外的需要小心的奇怪之處。由于ToPrimitive
抽象操作的工作方式,a + ""
在值a
上調用valueOf()
,它的返回值再最終通過內部的ToString
抽象操作轉換為一個string
。但是String(a)
只直接調用toString()
。
兩種方式的最終結果都是一個string
,但如果你使用一個object
而不是一個普通的基本類型number
的值,你可能不一定得到 相同的 string
值!
考慮這段代碼:
var a = {
valueOf: function() { return 42; },
toString: function() { return 4; }
};
a + ""; // "42"
String( a ); // "4"
一般來說這樣的坑不會咬到你,除非你真的試著創建令人困惑的數據結構和操作,但如果你為某些object
同時定義了你自己的valueOf()
和toString()
方法,你就應當小心,因為你強制轉換這些值的方式將影響到結果。
那么另外一個方向呢?我們如何將一個string
隱含強制轉換 為一個number
?
var a = "3.14";
var b = a - 0;
b; // 3.14
-
操作符是僅為數字減法定義的,所以a - 0
強制a
的值被轉換為一個number
。雖然少見得多,a * 1
或a / 1
也會得到相同的結果,因為這些操作符也是僅為數字操作定義的。
那么對-
操作符使用object
值會怎樣呢?和上面的+
的故事相似:
var a = [3];
var b = [1];
a - b; // 2
兩個array
值都不得不變為number
,但它們首先會被強制轉換為string
(使用意料之中的toString()
序列化),然后再強制轉換為number
,以便-
減法操作可以實施。
那么,string
和number
值之間的 隱含 強制轉換還是你總是在恐怖故事當中聽到的丑陋怪物嗎?我個人不這么認為。
比較b = String(a)
(明確的)和b = a + ""
(隱含的)。我認為在你的代碼中會出現兩種方式都有用的情況。當然b = a + ""
在JS程序中更常見一些,不管一般意義上 隱含 強制轉換的好處或害處的 感覺 如何,它都提供了自己的用途。