你可能忽略的js型別轉換

瀟湘待雨發表於2018-05-25

前言

相信剛開始瞭解js的時候,都會遇到 2 =='2',但 1+2 == 1+'2'為false的情況。這時候應該會是一臉懵逼的狀態,不得不感慨js弱型別的靈活讓人髮指,型別轉換就是這麼猝不及防。結合實際中的情況來看,有意或無意中涉及到型別轉換的情況還是很多的。既然要用到,就需要掌握其原理,知其然重要知其所以然更重要。

js的變數型別

JavaScript 是弱型別語言,意味著JavaScript 變數沒有預先確定的型別。
並且變數的型別是其值的型別。也就是說變數當前的型別由其值所決定,誇張點說上一秒種的string,下一秒可能就是個array了。此外當進行某些操作時,變數可以進行型別轉換,我們主動進行的就是顯式型別轉換,另一種就是隱式型別轉換了。例如:

var a = '1';   
typeof a;//string 

a =parseInt(a); //顯示轉換為number
typeof a  //number   

a == '1' //true
複製程式碼

弱型別的特性在給我們帶來便利的同時,也會給我們帶來困擾。趨利避害,充分利用該特性的前提就是掌握型別轉換的原理,下面一起看一下。

js資料型別

老生常談的兩大類資料型別:

  1. 原始型別
    Undefined、 Null、 String、 Number、 Boolean
  2. 引用型別
    object
    此外還有一個es6新增的Symbol,先不討論它。對於這五類原始型別,突然提問可能想不全,沒必要去死記硬背,可以想一下為否的常見變數及其對應值即可。
0 Number
'' String
false Boolean
null Null
undefined Undefined

對於不同的資料格式轉換規則是不同的,我們需要分別對待。

轉換規則

既然是規範定義的規則,那就不要問為什麼了,先大致看一下,爭取記住。是在不行經常翻翻看看大佬的部落格es5規範。轉換有下面這麼幾類,我們分別看一下具體規範。(這部分轉換規則,完全可以跳過去,看到下面的例項再回頭看應該更容易接受一些)

  • 轉換為原始值
  • 轉換為數字
  • 轉換為字串

ToPrimitive(轉換為原始值)

ToPrimitive 運算子接受一個值,和一個可選的 期望型別 作引數。ToPrimitive 運算子把其值引數轉換為非物件型別。如果物件有能力被轉換為不止一種原語型別,可以使用可選的 期望型別 來暗示那個型別。根據下表完成轉換
你可能忽略的js型別轉換

這段定義看起來有點枯燥。轉換為原始值,其實就是針對引用資料的,其目的是轉換為非物件型別。
如果已經是原始型別,當然就不做處理了
對於object,返回對應的原始型別,該原始型別是由期望型別決定的,期望型別其實就是我們傳遞的type。直接看下面比較清楚。
ToPrimitive方法大概長這麼個樣子具體如下。

/**
* @obj 需要轉換的物件
* @type 期望轉換為的原始資料型別,可選
*/
ToPrimitive(obj,type)
複製程式碼

type可以為number或者string,兩者的執行順序有一些差別
string:

  1. 呼叫obj的toString方法,如果為原始值,則返回,否則下一步
  2. 呼叫obj的valueOf方法,後續同上
  3. 丟擲TypeError 異常

number:

  1. 呼叫obj的valueOf方法,如果為原始值,則返回,否則下一步
  2. 呼叫obj的toString方法,後續同上
  3. 丟擲TypeError 異常

其實就是呼叫方法先後,畢竟期望資料型別不同,如果是string當然優先呼叫toString。反之亦然。
當然type引數可以為空,這時候type的預設值會按照下面的規則設定

  1. 該物件為Date,則type被設定為String
  2. 否則,type被設定為Number

對於Date資料型別,我們更多期望獲得的是其轉為時間後的字串,而非毫秒值,如果為number,則會取到對應的毫秒值,顯然字串使用更多。 其他型別物件按照取值的型別操作即可。

概括而言,ToPrimitive轉成何種原始型別,取決於type,type引數可選,若指定,則按照指定型別轉換,若不指定,預設根據實用情況分兩種情況,Date為string,其餘物件為number。那麼什麼時候會指定type型別呢,那就要看下面兩種轉換方式了。

toNumber

某些特定情況下需要用到ToNumber方法來轉成number 運算子根據下表將其引數轉換為數值型別的值 你可能忽略的js型別轉換

對於string型別,情況比較多,只要掌握常見的就行了。和直接呼叫Number(str)的結果一致,這裡就不多提了,主要是太多提不完。
需要注意的是,這裡呼叫ToPrimitive的時候,type就指定為number了。下面的toString則為string。

toString

ToString 運算子根據下表將其引數轉換為字串型別的值:
其實瞭解也很簡單,畢竟是個規範,借用大佬一張圖:
你可能忽略的js型別轉換

雖然是需要死記的東西,還是有些規律可循的。 對於原始值:

  • Undefined,null,boolean 直接加上引號,例如'null'
  • number 則有比較長的規範,畢竟範圍比較大
    常見的就是 '1' NaN則為'NaN' 基本等同於上面一條 對於負數,則返回-+字串 例如 '-2' 其他的先不考慮了。
  • 物件則是先轉為原始值,再按照上面的步驟進行處理。

valueOf

當呼叫 valueOf 方法,採用如下步驟:

  1. 呼叫ToObject方法得到一個物件O
  2. 原始資料型別轉換為對應的內建物件, 引用型別則不變
  3. 呼叫該物件(O)內建valueOf方法.

不同內建物件的valueOf實現:

  • String => 返回字串值
  • Number => 返回數字值
  • Date => 返回一個數字,即時間值,字串中內容是依賴於具體實現的
  • Boolean => 返回Boolean的this值
  • Object => 返回this

對照程式碼更清晰一點

var str = new String('123')
//123
console.log(str.valueOf())
var num = new Number(123)
//123
console.log(num.valueOf())
var date = new Date()
//1526990889729
console.log(date.valueOf())
var bool = new Boolean('123')
//true
console.log(bool.valueOf())
var obj = new Object({valueOf:()=>{
    return 1
}})
//依賴於內部實現
console.log(obj.valueOf())

複製程式碼

運算隱式轉換

前面提了那麼多抽象概念,就是為了這裡來理解具體轉換的。
對於+運算來說,規則如下:

  • +號左右分別進行取值,進行ToPrimitive()操作
  • 分別獲取左右轉換之後的值,如果存在String,則對其進行ToString處理後進行拼接操作。
  • 其他的都進行ToNumber處理
  • 在轉換時ToPrimitive,除去Date為string外都按照ToPrimitive type為Number進行處理 說的自己都迷糊了快,一起結合程式碼來看一下
1+'2'+false
複製程式碼
  1. 左邊取原始值,依舊是Number
  2. 中間為String,則都進行toString操作
  3. 左邊轉換按照toString的規則,返回'1'
  4. 得到結果temp值'12'
  5. 右邊布林值和temp同樣進行1步驟
  6. temp為string,則布林值也轉為string'false'
  7. 拼接兩者 得到最後結果 '12false'

我們看一個複雜的

var obj1 = {
    valueOf:function(){
        return 1
    }
}
var obj2 = {
    toString:function(){
        return 'a'
    }
}
//2
console.log(1+obj1)
//1a
console.log('1'+ obj2)
//1a
console.log(obj1+obj2)
複製程式碼

不管多複雜,按照上面的順序來吧。

  • 1+obj1
    1. 左邊就不說了,number
    2. 右邊obj轉為基礎型別,按照type為number進行
    3. 先呼叫valueOf() 得到結果為1
    4. 兩遍都是number,則進行相加得到2
  • 1+obj2
    1. 左邊為number
    2. 右邊同樣按照按照type為number進行轉化
    3. 呼叫obj2.valueOf()得到的不是原始值
    4. 呼叫toString() return 'a'
    5. 依據第二條規則,存在string,則都轉換為string進行拼接
    6. 得到結果1a
  • obj1+obj2
    1. 兩邊都是引用,進行轉換 ToPrimitive 預設type為number
    2. obj1.valueOf()為1 直接返回
    3. obj2.valueOf()得到的不是原始值
    4. 呼叫toString() return 'a'
    5. 依據第二條規則,存在string,則都轉換為string進行拼接
    6. 得到結果1a

到這裡相信大家對+這種運算的型別轉換了解的差不多了。下面就看一下另一種隱式型別轉換

== 抽象相等比較

這種比較分為兩大類,

  • 型別相同
  • 型別不同 相同的就不說了,隱式轉換髮生在不同型別之間。規律比較複雜,規範比較長,這裡也不列舉了,大家可以檢視抽象相等演算法。簡單總結一句,相等比較就不想+運算那樣string優先了,是以number優先順序為最高。概括而言就是,都儘量轉成number來進行處理,這樣也可以理解,畢竟比較還是期望比較數值。那麼規則大概如下:
    對於x == y
  1. 如果x,y均為number,直接比較

     沒什麼可解釋的了
     1 == 2 //false
    複製程式碼
  2. 如果存在物件,ToPrimitive() type為number進行轉換,再進行後面比較

    var obj1 = {
        valueOf:function(){
            return '1'
        }
    }
    1 == obj2  //true
    //obj1轉為原始值,呼叫obj1.valueOf()
    //返回原始值'1'
    //'1'toNumber得到 1 然後比較 1 == 1
    [] == ![] //true
    //[]作為物件ToPrimitive得到 ''  
    //![]作為boolean轉換得到0 
    //'' == 0 
    //轉換為 0==0 //true
    複製程式碼
  3. 存在boolean,按照ToNumber將boolean轉換為1或者0,再進行後面比較

    //boolean 先轉成number,按照上面的規則得到1  
    //3 == 1 false
    //0 == 0 true
    3 == true // false
    '0' == false //true 
    複製程式碼
  4. 如果x為string,y為number,x轉成number進行比較

    //'0' toNumber()得到 0  
    //0 == 0 true
    '0' == 0 //true 
    複製程式碼

結束語

參考文章

ECMAScript5.1中文版 + ECMAScript3 + ECMAScript(合集)
你所忽略的js隱式轉換
這篇文章的本意是為自己解惑,寫到後面真的感覺比較乏味,畢竟規範性的東西多一點,不過深入瞭解一下總好過死記硬背。原文請移步我的部落格。對於有些觀點說這些屬於js糟粕,完全不應該深入,怎麼說呢,結合自己情況判斷吧。本人水平有限,拋磚引玉共同學習。

相關文章