強制類型轉換
值類型轉換
將值從一種類型轉換為另一種類型通常稱為類型轉換(type casting),這是顯示的情況;隱式的情況稱為強制類型轉換(coercion)。
也可以這樣來區分:類型轉換發生在靜態類型語言的編譯階段,而強制類型轉換則發生在動態類型語言的運行時。
然而在 js 中通常將他們統稱為強制類型轉換,我個人則傾向于用“隱式強制類型轉換”和“顯示強制類型轉換”來區分。
二者的區別顯而易見:我們能夠從代碼中看出那些地方是顯示強制類型轉換,而隱式強制類型轉換則不那么明顯,通常式某些操作產生的副作用。
例如:
var a = 42;
var b = a + ""; // 隱式強制類型轉換
var c = String( a ); // 顯示強制類型轉換
抽象值操作
1.ToString
該抽象操作負責處理非字符串到字符串的強制類型轉換。
基本類型值的字符串化規則為:null 轉換為 "null",undefined 轉換為 "undefined",true 轉換為 "true"。數字的字符串化則遵循通用規則,不過之前講過的 那些極小和極大的數字使用指數形式。
對普通對象來說,除非自己定義,否則 toString() (Object.prototype.toString()) 返回內部屬性 [[Class]] 的值,如 "[object Object]"。
然而前面我們介紹過,如果對象有自己的 toString() 方法,字符串化時就會調用該方法并使用其返回值。
數組的默認 toString() 方法經過了重新定義:
var a = [1,2,3];
a.toString(); // "1,2,3"
JSON 字符串化
工具函數 JSON.stringify(..) 在將 JSON 對象序列化時也用到了 ToString。
請注意,JSON 字符串化并非嚴格意義上的強制類型轉換,因為其中也設計了 ToString 的相關規則。
對于大多數簡單值來說, JSON 字符串化和 toString() 的效果基本相同,只不過序列化的結果總是字符串:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42""(含有雙引號的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
所有安全的 JSON 值都可以使用 JSON.stringify(..) 字符串化。安全的 JSON 值始值能夠呈現為有效 JSON 格式的值。
為了簡單起見,我們來看看什么是不安全的 JSON 值。undefined、function、symbol 和包含循環引用的對象都不符合 JSON 的結構標準,其他支持 JSON 的語言都無法處理它們。
JSON.stringify(..) 在對象中遇到 undefined、function 和 symbol 時會自動將其忽略,在數組中則會返回 null,以保證單元位置不變。
例如:
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(..) 會出錯。
如果對象定義了 toJSON() 方法,JSON 字符串化時會首先調用該方法,然后用它的返回值來進行序列化。
如果要對含有非法的 JSON 值得對象做字符串化,或者對象中的某些值無法被序列化時,就需要定義 toJSON() 方法來返回一個安全的 JSON 值。
例如:
var o = {};
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中創建一個循環引用
o.e = a;
// 循環引用在這里會產生錯誤
JSON.stringify( a );
// 自定義的 JSON 序列化
a.toJSON = function(){
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
很多人誤以為 toJSON 返回的是 JSON 字符串化后的值,其實不然,除非我們確實想要對字符串進行字符串化(通常不會!)。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]""
現在介紹幾個不太為人所知但卻非常有用的功能。
我們可以向 JSON.stringify(..) 傳遞一個可選參數 replacer,它可以是數組或者函數,用來指定對象序列化過程中哪些屬性應該被處理,哪些應該被排除,和 toJSON() 很像。
如果 replacer 是一個數組,那么它必須是一個字符串數組,其中包含序列化要處理的對象的屬性名稱,除此以外的其他屬性則被忽略。
如果 replacer 是一個函數,它會對對象本身調用一次,然后對對象中的每個屬性各調用一次,每次傳遞兩個參數,鍵和值。如果要忽略某個鍵就返回 undefined,否則返回指定的值。
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]}"
JSON.stringify 還有一個可選參數 space,用來指定輸出的縮進格式。
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 強制類型轉換,具體表現在以下兩點。
1.字符串、數字、布爾值和 null 的 JSON.stringify(..) 規則與 ToString 基本相同。
2.如果傳遞給 JSON.stringify(..) 的對象中定義了 toJSON() 方法,那么該方法會在字符串化前調用,以便將對象轉換為安全的值
2.ToNumber
其中 true 轉換為 1, false 轉換為 0。undefined 轉換為 NaN,null 轉換為 0。
ToNumber 對字符串的處理基本遵循數字常量的相關規則 / 語法。處理失敗時返回 NaN。
對象(包括數組)會首先被轉換為相應的基本類型值,如果返回的是非數字的基本類型值,則再遵循以上規則將其強制轉換為數字。
為了將值轉換為相應的基本類型值,抽象操作 ToPrimitive 會首先檢查該值是否有 valueOf() 方法。如果有并且返回基本類型值,就使用該值進行強制類型轉換。如果沒有就使用 toString() 的返回值來進行強制類型轉換。
如果 valueOf() 和 toString() 均不返回基本類型值,會產生 TypeError 錯誤。
從 ES5 開始,使用 Object.create(null) 創建的對象 [[Prototype]] 屬性為 null,并且沒有 valueOf() 和 toString() 方法,因此無法進行強制類型轉換。
我們稍后會詳細介紹數字的強制類型轉換,下面的示例代碼中我們假定 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
3.ToBoolean
首先也是最重要的一點是,js 中有兩個關鍵詞 true 和 false,分別代表布爾類型中的真和假。我們常誤以為值 1 和 0 分別等同于 true 和 false。在有些語言中可能是這樣,但在 js 中布爾值和數字是不一樣的。雖然我們可以將 1 強制類型轉換為 true,將 0 強制類型轉換為 false,反之亦然,但它們并不是一回事。
- 假值(falsy value)
js 中的值可以分為以下兩類:
1)可以被強制類型轉換為 false 的值
2)其他(被強制類型轉換為 true 的值)
js 規范具體定義了一小撮可以被強制類型轉換為 false 的值
以下是這些假值:
undefined、null、false、+0、-0、NaN 和 ""
假值的布爾強制類型轉換結果為 false。
從邏輯上說,價值列表以外的都應該是真值(truthy)。但 js 規范對次并沒有明確定義,只是給出了一些示例,例如規定所有的對象都是真值。 - 假值對象(falsy object)
這個標題似乎有點自相矛盾。前面講過規范規定所有的對象都是真值,怎么還有價值對象呢?
有些人可能會以為假值對象就是包裝了假值的封裝對象,其實不然!
例如:
var a = new Boolean( false );
var b = new Number( 0 );
var c = new String( "" );
var d = Boolean( a && b && c);
d; // true
d 為 true,說明 a、b、c 都為 true。
請注意,這里 Boolean(..) 對 a && b && c 進行了封裝,有人可能問為什么。我們暫且記下,稍后會做說明。你可以試試不用 Boolean(..) 的話 d = a && b && c 會產生什么結果。
如果價值對象并非封裝了假值的對象,那它究竟是什么?
值得注意的是,雖然 js 代碼會出現假值對象,但它實際上并不屬于 js 語言的范疇。
瀏覽器在某些特定情況下,在常規 js 語法基礎上自己創建了一些外來值,這些就是“假值對象”。
假值對象看起來和普通對象并無二致(都有屬性,等等),但將它們強制類型轉換為布爾值時結果為 false。
最常見的例子是 document.all,它是一個類數組對象,包含了頁面上的所有元素,由 DOM 提供給 js 程序使用。
那為什么它是假值呢?因為我們經常通過將 document.all 強制類型轉換為布爾值來··判斷瀏覽器是否是老版本的 IE。
if(document.all) { /* it's old version IE */ }
- 真值(truthy value)
真值就是假值列表之外的值。例如:
var a = "false";
var b = "0";
var c = "''";
var d = Boolean(a && b && c);
d; // true
再如:
var a = [];
var b = {};
var c = function(){};
var d = Boolean( a && b && c );
d; // true
顯示強制類型轉換
顯示強制類型轉換是那些顯而易見的類型轉換,很多類型轉換都屬于此列。
對顯示強制類型轉換幾乎不存在非議,它類似于靜態語言中的類型轉換,已被廣泛接受,不會有什么坑。我們后面再討論這個話題。
字符串和數字之間的顯示轉換
我們從最常見的字符串和數字之間的強制類型轉換開始
字符串和數字之間的轉換是通過 String(..) 和 Number(..) 兩個內建函數(原生構造函數)來實現的,請注意它們前面沒有 new 關鍵字,并不創建封裝對象。
下面是兩者之間的顯示強制類型轉換:
var a = 42;
var b = String( a );
var c = "3.14";
var d = Number( c );
b; // "42"
d; // 3.14
除了 String(..) 和 Number(..) 以外,還有其他方法可以實現字符串和數字之間的顯示轉換:
var a = 42;
var b = a.toString();
var c = "3.14";
var d = +c;
b; // "42"
d; // 3.14
一元運算符 +c 可以將 c 轉換為數字,而非數字的加法運算。
不過有時也容易產生誤會。例如:
var c = "3.14";
var d = 5+ +c;
d; // 8.14
一元運算符 - 和 + 一樣,并且它還會反轉數字的符號位。由于 -- 會被當作遞減運算符來處理,所以我們不能使用 -- 來撤銷反轉,而應該像 - -"3.14" 這樣,在中間加一個空格,才能得到正確結果 3.14。
1.日期顯示轉換為數字
一元運算符 + 的另一個常見的用途是將日期對象強制類型轉換為數字,返回的結果為 Unix 時間戳,以毫秒為單位:
var d = new Date( "Mon, 18 Aug 2014 08:53:06 CDT" );
+d; // 1408369986000
我們常用下面的方法獲得當前的時間戳:
var timestamp = +new Date();
js 有一處奇特的語法,即構造函數沒有參數時可以不用帶()。
于是我們可能會碰到 var timestamp = +new Date;
這樣的寫法。
不過最好還是使用 ES5 中新加入的靜態方法 Date.now()。我們不建議對日期類型使用強制類型轉換。
2.奇特的 ~ 運算符
一個常被人忽視的地方是 ~ 運算符(即字位操作“非”)相關的強制類型轉換。
字位操作符只適用于32位整數,運算符會強制操作數使用32位格式。這是通過抽象操作 ToInt32 來實現的。
ToInt32 首先執行 ToNumber 強制類型轉換,比如 "123" 會先轉換為123,然后再執行 ToInt32。
雖然嚴格說來并非強制類型轉換(因為返回值類型并沒有發生變化),但字位運算符(如 | 和 ~)和某些特殊數字一起使用時會產生類似強制類型轉換的效果,返回另外一個數字。
例如 | 運算符(字位操作“或”)的空操作 0 | x,它僅執行 ToInt32 轉換:
0 | -0; //0
0 | NaN; // 0
0 | Infinity; // 0
0 | -Infinity; // 0
以上這些特殊數字無法以32位呈現(因為它們來自 64 位 IEEE 754 標準),因此返回 0。
再回到 ~。它首先將值強制類型轉換為32位數字,然后執行字位操作“非”(對每一個字位進行反轉)。
對 ~ 還可以有另外一種詮釋,源自早期的計算機科學和離散數學:~ 返回 2 的補碼。這樣一來問題就清楚多了!
~x 大致等同于 -(x+1)。
很奇怪,但相對更容易說明問題。
~42; // -(42+1) ==> -43
另外,在 -(x+1) 中唯一能都得到 0(或者嚴格說是 -0)的 x 值是 -1。也就是說如果 x 為-1時, ~ 和一些數字值在一起會返回假值0,其他情況則返回真值。
這個特點很有用處,因為 -1 是一個“哨位值”,即被賦予了特殊含義的值,在 C 語言中我們用 -1 來表示函數執行失敗,用大于等于 0 的值表示函數執行成功。
js 中字符串的 indexOf(..) 方法也遵循這一慣例,該方法在字符串中搜索指定的子字符串,如果找到就返回子字符串的位置(從0開始),否則返回 -1。
indexOf(..) 不僅能夠得到子字符串的位置,還可以用來檢查字符串中是否包含指定的子字符串,相當于一個條件判斷。
例如:
var a = "Hello World";
if (a.indexOf( "lo" ) >= 0){ // true
// 找到匹配!
}
if (a.indexOf("lo") != -1){ // true
// 找到匹配!
}
if (a.indexOf("lo") < 0){ // true
// 沒有找到匹配!
}
if (a.indexOf("lo") == -1){ // true
// 沒有找到匹配!
}
大于等于 0 和 ==-1 這樣的寫法不是很好,成為“抽象滲漏”,意思是在代碼中暴露了底層的實現細節,這里是指用 -1 作為失敗時的返回值,這些細節應該被屏蔽掉。
現在我們終于明白 ~ 的用處了!~ 和 indexOf() 一起可以將結果強制類型轉換為 真 / 假值:
var a = "Hello World";
~a.indexOf( "lo" ); // -4 <--真值!
if(~a.indexOf( "lo" )){ // true
// 找到匹配!
}
~a.indexOf( "ol" ); // 0 <--假值!
!~a.indexOf( "ol" ); // true
if(!~a.indexOf( "ol" )){ // true
// 沒有找到匹配!
}
由 -(x+1) 推斷 ~-1 的結果應該是 -0,然而實際上結果是0,因為它是字位操作而非數字運算。
3.字位截除
一些開發人員使用 ~~ 來截除數字值得小數部分,以為這和 Math.floor(..) 的效果一樣,實際上并非如此。
~~ 中的第一個 ~ 執行 ToInt32 并反轉字位,然后第二個 ~ 再進行一次字位反轉,即將所有的字位反轉回原值,最后得到的仍然是 ToInt32 的結果。
~~ 和 !! 很相似
對 ~~ 我們要多加注意。首先它只適用于 32 位數字,更重要的是它對負數的處理與 Math.floor(..) 不同。
Math.floor( -49.6 ); // -50
~~-49.6; // -49
~~x 能將值截除為一個32位整數,x | 0 也可以,而且看起來還更簡潔。
出于對運算優先級的考慮,我們更傾向于使用 ~~x:
~~1E20 / 10; // 166199296
1E20 | 0 / 10; // 1661992960
(1E20 | 0 )/ 10; // 166199296
顯示解析數字字符串
例如:
var a = "42";
var b = "42px";
Number( a ); // 42
parseInt( a ); // 42
Number( b ); // NaN
parseInt( b ); // 42
解析允許字符串中含有非數字字符,解析從左到右進行,如果遇到非數字字符就停止解析。而轉換不允許出現數字字符,否則會失敗并返回 NaN。
解析字符串中的浮點數可以使用 parseFloat(..) 函數。
不要忘了 parseInt(..) 針對的是字符串值。向 parseInt(..) 傳遞數字和其他類型的參數是沒有用的,比如 true、function(){...} 和 [1,2,3]。
非字符串參數會首先被強制類型轉換位字符串,依賴這樣的隱式強制類型轉換并非上策,應該避免向 parseInt(..) 傳遞非字符串參數。
從 ES5 開始 parseInt(..) 默認轉換位十進制數,除非另外指定。如果你的代碼需要在 ES5 之前的環境運行,請記得將第二個參數設置為 10。
parseInt 解析非字符串
例如:
parseInt( 1/0, 19 ); // 18
parseInt( 1/0, 19 ) 實際上是 parseInt("Infinity", 19)。第一個字符是 "I",以 19 為基數時值為 18。第二個字符 "n" 不是一個有效的數字字符,解析到此為止。
此外還有一些看起來很奇怪但實際上能解釋得通的例子:
parseInt( 0.000008 ); // 0 ("0" 來自于 "0.000008")
parseInt( 0.0000008 ); // 8 ("8" 來自于 "8e-7")
parseInt( false, 16 ); // 250 ("fa" 來自于 "false")
parseInt( parseInt, 16); // 15 ("f" 來自于 "function..")
parseInt( "0x10" ); // 16
parseInt( "103", 2 ); // 2
其實 parseInt(..) 函數是十分靠譜的,只要使用得當就不會有問題。因為使用不等而導致一些莫名奇妙的結果,并不能歸咎與 js 本身。
顯示轉換位布爾值
與前面的 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
和前面講的 + 類似,一元運算符 ! 顯示地將值強制類型轉換為布爾值。但是它同時還將真值反轉為假值(或者將假值反轉為真值)。所以顯示強制類型轉換為布爾值最常用的方法是 !!,因為第二個 ! 會將結果反轉為原值:
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
顯示 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]"
隱式強制類型轉換
隱式強制類型轉換指的是那些隱蔽的強制類型轉換,副作用也不是很明顯。
顯式強制類型轉換旨在讓代碼更加清晰可讀,而隱式強制類型轉換看起來就像是它的對立面,會讓代碼變得晦澀難懂。
對強制類型轉換的詬病大多是針對隱式強制類型轉換。
但是隱式強制類型轉換的作用是減少冗余,讓代碼更簡潔。
字符串和數字之間的隱式強制類型轉換
通過重載,+ 運算符即能用于數字加法,也能用于字符串拼接。js 怎樣來判斷我們要的是哪個操作?例如: