徹底理解js中的數(shù)據(jù)類型與類型轉(zhuǎn)換

從類型說起

js只有7種類型:

  • 原始類型(primitives types)
    • boolean
    • number
      • 包括Infinity和NaN,你可以通過typeof Infinity;來驗(yàn)證
    • string
    • null
    • undefined
    • Symbol (ECMAScript 6 新定義,暫時(shí)用不上,這篇文章不討論)
  • Object 類型
    • js內(nèi)置了很多對象供你使用,MDN文檔將它們?nèi)苛信e了出來(當(dāng)然,我們經(jīng)常使用的只是其中的一部分)。

注意,上面這個(gè)MDN鏈接中給出的“值屬性”那一欄中的值并不是對象。

原始類型的值是不會(huì)改變的。你可以給變量賦予不同的原始值,只不過是讓變量指向了內(nèi)存中的另外一個(gè)原始值,但是原本的那個(gè)原始值在內(nèi)存中并沒有變化。

Object類型就不一樣,通過obj.k = '另一個(gè)值',在內(nèi)存中存儲(chǔ)obj的一些字節(jié)確確實(shí)實(shí)地被改變了。

有關(guān)原始類型和對象類型在變量中是如何存儲(chǔ)的,可以看我之前寫的一篇文章。

js的自動(dòng)裝箱

雖然string是原始類型,但為什么我們好像可以調(diào)用“string的函數(shù)”呢?原始類型不應(yīng)該有函數(shù)?。?/p>

var str = 'I am str';
str.toUpperCase();  // "I AM STR"

原因是js標(biāo)準(zhǔn)庫給boolean、number、string分別提供了一個(gè)包裝對象:Boolean
、Number
、String
。在需要的時(shí)候,原始類型會(huì)自動(dòng)轉(zhuǎn)換成相應(yīng)的包裝對象(這個(gè)過程叫自動(dòng)裝箱)。上例的toUpperCase就是String標(biāo)準(zhǔn)對象定義的一個(gè)函數(shù)。

自動(dòng)裝箱就是臨時(shí)創(chuàng)建一個(gè)包裝對象,將原始類型的值封裝起來,以便調(diào)用包裝對象的函數(shù)。但是原來那個(gè)變量的值不會(huì)有任何變化!執(zhí)行完上面例子的代碼之后,str指向的依然是那個(gè)原始值:

typeof str;  // "string"

當(dāng)然,你可以將Boolean 、Number 、String 這三個(gè)函數(shù)當(dāng)作構(gòu)造函數(shù)來使用,通過手動(dòng)new一個(gè)包裝類來裝箱

var str_object = new String('I am str_object');  //  手動(dòng)裝箱
str_object.toUpperCase();  //  "I AM STR_OBJECT"
typeof str_object;  //  "object"

在文章的后面,我們還會(huì)將這三個(gè)函數(shù)當(dāng)作普通的函數(shù)使用,實(shí)現(xiàn)強(qiáng)制類型轉(zhuǎn)換。

兩個(gè)與類型轉(zhuǎn)換有關(guān)的函數(shù):valueOf()和toString()

  • valueOf()的意義是,返回這個(gè)對象邏輯上對應(yīng)的原始類型的值。比如說,String包裝對象的valueOf(),應(yīng)該返回這個(gè)對象所包裝的字符串。
  • toString()的意義是,返回這個(gè)對象的字符串表示。用一個(gè)字符串來描述這個(gè)對象的內(nèi)容。

valueOf()和toString()是定義在Object.prototype上的方法,也就是說,所有的對象都會(huì)繼承到這兩個(gè)方法。但是在Object.prototype上定義的這兩個(gè)方法往往不能滿足我們的需求(Object.prototype.valueOf()僅僅返回對象本身),因此js的許多內(nèi)置對象都重寫了這兩個(gè)函數(shù),以實(shí)現(xiàn)更適合自身的功能需要(比如說,String.prototype.valueOf就覆蓋了在Object.prototype中定義的valueOf)。當(dāng)我們自定義對象的時(shí)候,最好也重寫這個(gè)方法。

以下是部分內(nèi)置對象調(diào)用valueOf()的行為:

對象 返回值
Array 數(shù)組本身(對象類型)。
Boolean 布爾值(原始類型)。
Date 從 UTC 1970 年 1 月 1 日午夜開始計(jì)算,到所封裝的日期所經(jīng)過的毫秒數(shù)(原始類型)。
Function 函數(shù)本身(對象類型)。
Number 數(shù)字值(原始類型)。
Object 對象本身(對象類型)。如果自定義對象沒有重寫valueOf方法,就會(huì)使用它。
String 字符串值(原始類型)。

由上表可見,valueOf()雖然期望返回原始類型的值,但是實(shí)際上有一些對象在邏輯上無法找到與之對應(yīng)的原始值,因此只能返回對象本身。

toString()則不一樣,因?yàn)?strong>不管什么對象,我們總有辦法“描述”它,因此js內(nèi)置對象的toString()總能返回一個(gè)原始string類型的值。

var d = new Date();
d.toString()
// "Fri Apr 21 2017 14:54:04 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)"

我們自己在重寫toString()的時(shí)候也應(yīng)該返回合理的string。

valueOf()和toString()經(jīng)常會(huì)在類型轉(zhuǎn)換的時(shí)候被js內(nèi)部調(diào)用,比如說我們后文會(huì)談到的ToPrimitive。在自定義對象上合理地覆蓋valueOf()和toString(),可以控制自定義對象的類型轉(zhuǎn)換。

js內(nèi)部用于實(shí)現(xiàn)類型轉(zhuǎn)換的4個(gè)函數(shù)

這4個(gè)方法實(shí)際上是ECMAScript定義的4個(gè)抽象的操作,它們在js內(nèi)部使用,進(jìn)行類型轉(zhuǎn)換。我們js的使用者不能直接調(diào)用這些函數(shù),但是了解這些函數(shù)有利于我們理解js類型轉(zhuǎn)換的原理。

  • ToPrimitive ( input [ , PreferredType ] )
  • ToBoolean ( argument )
  • ToNumber ( argument )
  • ToString ( argument )

請區(qū)分這里的ToString()和上文談到的toString(),一個(gè)是js引擎內(nèi)部使用的函數(shù),另一個(gè)是定義在對象上的函數(shù)。

ToPrimitive ( input [ , PreferredType ] )

將input轉(zhuǎn)化成一個(gè)原始類型的值。PreferredType參數(shù)要么不傳入,要么是Number 或 String。如果PreferredType參數(shù)是Number,ToPrimitive這樣執(zhí)行:

  1. 如果input本身就是原始類型,直接返回input。
  2. 調(diào)用input.valueOf(),如果結(jié)果是原始類型,則返回這個(gè)結(jié)果。
  3. 調(diào)用input.toString(),如果結(jié)果是原始類型,則返回這個(gè)結(jié)果。
  4. 拋出TypeError異常。

以下是PreferredType不為Number時(shí)的執(zhí)行順序。

  • 如果PreferredType參數(shù)是String,則交換上面這個(gè)過程的第2和第3步的順序,其他執(zhí)行過程相同。
  • 如果PreferredType參數(shù)沒有傳入
    • 如果input是內(nèi)置的Date類型,PreferredType 視為String
    • 否則PreferredType 視為 Number

可以看出,ToPrimitive依賴于valueOf和toString的實(shí)現(xiàn)。

ToBoolean ( argument )

Argument Type Result
Undefined Return false
Null Return false
Boolean Return argument
Number 僅當(dāng)argument為 +0, -0, or NaN時(shí), return false; 否則一律 return true
String 僅當(dāng)argument是空字符串(長度為0)時(shí), return false; 否則一律 return true
Symbol Return true
Object Return true

這些規(guī)定都來自ECMA的標(biāo)準(zhǔn),js內(nèi)部就是這樣實(shí)現(xiàn)的。
只需要記憶幾種返回false的情況就可以了,其他一律返回true。

ToNumber ( argument )

Argument Type Result
Undefined Return NaN
Null Return +0
Boolean 如果 argument 為 true, return 1. 如果 argument 為 false, return +0
Number 直接返回argument
String 將字符串中的內(nèi)容轉(zhuǎn)化為數(shù)字(比如"23"->23),如果轉(zhuǎn)化失敗則返回NaN(比如"23a"->NaN)
Symbol 拋出 TypeError 異常
Object primValue = ToPrimitive(argument, Number),再對primValue 使用 ToNumber(primValue)

由上表可見ToNumber的轉(zhuǎn)化并不總是成功,有時(shí)會(huì)轉(zhuǎn)化成NaN,有時(shí)則直接拋出異常。

ToString ( argument )

Argument Type Result
Undefined Return "undefined"
Null Return "null"
Boolean 如果 argument 為 true, return "true".如果 argument 為 false, return "false"
Number 用字符串來表示這個(gè)數(shù)字
String 直接返回 argument
Symbol 拋出 TypeError 異常
Object 先primValue = ToPrimitive(argument, hint String),再對primValue使用ToString(primValue)

隱式類型轉(zhuǎn)換(自動(dòng)類型轉(zhuǎn)換)

當(dāng)js期望得到某種類型的值,而實(shí)際在那里的值是其他的類型,就會(huì)發(fā)生隱式類型轉(zhuǎn)換。系統(tǒng)內(nèi)部會(huì)自動(dòng)調(diào)用我們前面說ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument ),嘗試轉(zhuǎn)換成期望的數(shù)據(jù)類型。

例子1:

if ( !undefined
  && !null
  && !0
  && !NaN
  && !''
) {
  console.log('true');
} // true

例子1:因?yàn)樵趇f的括號中,js期望得到boolean的值,所以對括號中每一個(gè)值都使用ToBoolean ( argument ),將它們轉(zhuǎn)化成boolean。

例子2:

3 * { valueOf: function () { return 5 } };  //15

例子2:因?yàn)樵诔颂柕膬啥?,js期望得到number類型的值,所以對右邊的那個(gè)對象使用ToNumber ( argument ),得到結(jié)果5,再與乘號左邊的3相乘。

例子3:

> function returnObject() { return {} }
> 3 * { valueOf: function () { return {} }, toString: function () { return {} } }
// TypeError: Cannot convert object to primitive value

例子3:調(diào)用ToNumber ( argument )的過程中,調(diào)用了ToPrimitive ( input , Number ),因?yàn)樵赥oPrimitive中valueOf和toString都沒有返回原始類型,所以拋出異常。

符號'+'是一個(gè)比較棘手的一個(gè)符號,因?yàn)樗瓤梢员硎尽八銛?shù)加法”,也可以表示“字符串拼接”。
簡單理解版本:只要'+'兩端的任意一個(gè)操作數(shù)是字符串,那么這個(gè)'+'就表示字符串拼接,否則表示算數(shù)加法。

12+3
// 15
12+'3'
// "123"

原理理解版本:根據(jù)ECMAScript的定義,對'+'運(yùn)算的求值按照以下過程:

  1. 令lval = 符號左邊的值,rval = 符號右邊的值
  2. 令lprim = ToPrimitive(lval),rprim = ToPrimitive(rval)
    • 如果lprim和rprim中有任意一個(gè)為string類型,將ToString(lprim)和ToString(rprim)的結(jié)果做字符串拼接
  • 否則,將ToNumber(lprim)和ToNumber(rprim)的結(jié)果做算數(shù)加法

根據(jù)這個(gè)原理可以解釋

[]+[]
//  ""
// 提示:ToPrimitive([])返回空字符串

[] + {}
//  "[object Object]"
//  提示:ToPrimitive({})返回"[object Object]"

123 + { toString: function () { return "def" } }
//  "123def"
//  提示:ToPrimitive(加號右邊的對象)返回"def"

{} + []
//  0
// 結(jié)果不符合我們的預(yù)期:"[object Object]"
// 提示:在Chrome中,符號左邊的{}被解釋成了一個(gè)語句塊,而不是一個(gè)對象
// 注意在別的執(zhí)行引擎上可能會(huì)將{}解釋成對象
//  這一行等價(jià)于'+[]'
// '+anyValue'等價(jià)于Number(anyValue)

({}) + []
//  "[object Object]"
// 加上括號以后,{}被解釋成了一個(gè)對象,結(jié)果符合我們的預(yù)期了

'<'、'>'的情況與'+'類似,但是處理方式與'+'有些不同。如果好奇請自行查閱文檔。

顯式類型轉(zhuǎn)換(強(qiáng)制類型轉(zhuǎn)換)

程序員顯式調(diào)用Boolean(value)、Number(value)、String(value)完成的類型轉(zhuǎn)換,叫做顯示類型轉(zhuǎn)換。
我們在文章的前面說過new Boolean(value)、new Number(value)、new String(value)傳入各自對應(yīng)的原始類型的值,可以實(shí)現(xiàn)“裝箱”——將原始類型封裝成一個(gè)對象。其實(shí)這三個(gè)函數(shù)不僅僅可以當(dāng)作構(gòu)造函數(shù),它們可以直接當(dāng)作普通的函數(shù)來使用,將任何類型的參數(shù)轉(zhuǎn)化成原始類型的值:

Boolean('sdfsd');  //  true
Number("23");  //  23
String({a:24});  //  "[object Object]"

其實(shí)這三個(gè)函數(shù)用于類型轉(zhuǎn)換的時(shí)候,調(diào)用的就是js內(nèi)部的ToBoolean ( argument )、ToNumber ( argument )、ToString ( argument )方法!

這里解釋一下String({a:24}); // "[object Object]"的過程:

  • 執(zhí)行String({a:24})
    • 執(zhí)行js內(nèi)部函數(shù)ToString ( {a:24} )
      • 執(zhí)行primValue = ToPrimitive({a:24}, hint String)
        1. 因?yàn)閧a:24}不是原始類型,進(jìn)入下一步。
        2. 在ToPrimitive內(nèi)調(diào)用({a:24}).toString(),返回了原始值"[object Object]",因此直接返回這個(gè)字符串,ToPrimitive后面的步驟不用進(jìn)行下去了。
      • primValue被賦值為ToPrimitive的返回值:"[object Object]"
      • 執(zhí)行js內(nèi)部函數(shù)ToString ( "[object Object]" ),返回"[object Object]"
      • 返回"[object Object]"
    • 返回"[object Object]"
  • 返回"[object Object]"

為了防止出現(xiàn)意料之外的結(jié)果,最好在不確定的地方使用顯式類型轉(zhuǎn)換。


參考文章:
ECMAScript類型轉(zhuǎn)換規(guī)范
What is {} + {} in JavaScript?
JavaScript quirk 1: implicit conversion of values
阮一峰的js教程
Object.prototype.toString()的原理
改變Object.prototype.toString.call(myClass)的輸出

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 強(qiáng)制轉(zhuǎn)換 強(qiáng)制轉(zhuǎn)換主要指使用Number、String和Boolean三個(gè)構(gòu)造函數(shù),手動(dòng)將各種類型的值,轉(zhuǎn)換成數(shù)字...
    燈火闌珊Zone閱讀 533評論 0 3
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 2,669評論 1 17
  • 9.正則表達(dá)式 首先,js定義了RegExp()構(gòu)造函數(shù),用來創(chuàng)建表示文本匹配模式的對象。這就是正則表達(dá)式。正則表...
    我就是z閱讀 822評論 0 5
  • 丁酉仲秋,宴八荒四海客,席上有客聞鈴,鈴引客去,歸途悄隱,客驚,鈴現(xiàn),鈴持于一覡,驟止鈴,漫光散,客行,失而墜,見...
    轅鳳閱讀 607評論 0 0
  • 迅速洗漱完畢,走到二樓,突然想要天氣預(yù)報(bào)說今天有中雨,探頭看樓下沒有雨,就繼續(xù)下樓。還是
    富思竭慮閱讀 294評論 0 0

友情鏈接更多精彩內(nèi)容