javascript強制型別轉換與操作符

幻靈爾依發表於2019-04-28

也許大家會覺得js強制型別轉換與操作符知道個大概就夠了。是的,對於搬磚工來說,夠了。但是作為一個有追求的前端,我深信只有掌握了別人不願意掌握的東西,才能讓自己變得更強大更有競爭力。

小綱老師

型別轉換規則

或許大家不喜歡隱式型別轉換,覺得這東西太沒人性。但是你有沒有想過,這也許正是js語言的獨特之處?我認同kyle大佬說的,如果你徹底掌握了隱式型別轉換,那麼對你來說,它就是“顯式”型別轉換了。

之所以先講型別轉換,是因為在操作符運算中涉及了大量的隱式型別轉換。

0. ToPrimitive

抽象操作ToPrimitive用於將引用型別轉為原始型別。實現細節比較複雜,有興趣的童鞋可以參考這裡

//模擬一個物件的轉基本型別操作 ToPrimitive
var o = {};
o[Symbol.toPrimitive] = function(hint) {
  console.log(hint) //hint字串至為 string number default 中的一個
  if (hint == "default" || hint == "number") {
    if (o.valueOf && typeof(o.valueof()) != 'object') {
      return o.valueOf()
    } else {
      return o.toString()
    }
  } else {
    if (o.toString && typeof(o.toString()) != 'object') {
      return o.toString()
    } else {
      return o.valueOf()
    }  
  }
}
String(o) // string
Number(o) // number
1+o // default
1-o // number
o++ // number
++o // number
複製程式碼

規則如下:

  1. 如果傳入引數是string(目前只有呼叫String()函式是執行這個順序):首先檢查該值是否有toString()方法。如果有並且返回基本型別值,就使用該值進行強制型別轉換。如果沒有就檢查該值是否有valueOf()方法。如果有並且返回基本型別值就使用該回值來進行強制型別轉換,如果沒有或者返回的不是基本型別值,就丟擲錯誤。
  2. 如果傳入引數是number/default(常見強制型別轉換都是這個順序):首先檢查該值是否有valueOf()方法。如果有並且返回基本型別值,就使用該值進行強制型別轉換。如果沒有就檢查該值是否有toString()方法。如果有並且返回基本型別值就使用該回值來進行強制型別轉換,如果沒有或者返回的不是基本型別值,就丟擲錯誤。

1. ToString

抽象操作 ToString,負責處理非字串到字串的強制型別轉換。當需要一個值的字串形式,就會進行 ToString 型別轉換。

String()函式就會執行抽象操作 ToString,遵循下列轉換規則:

  1. 如果值是基本型別,則直接轉為字串。如果是引用型別,則執行ToPrimitive抽象操作;
  2. 如果值是 null,則返回"null";
  3. 如果值是 undefined,則返回"undefined"。
String()  // ''
String(0)   // '0'
String(true)  // 'true'
String(null)  // 'null'
String(undefined)  // 'undefined'
String(Symbol('asdf'))  // "Symbol('asdf')"
String({})  // '[Object object]'
// 陣列的預設 toString() 方法經過了重新定義,將所有單元字串化以後再用 "," 連線起來
String([])  // ''
String([1,2,3,4,'asdf'])  // '1,2,3,4,asdf'
複製程式碼

2. ToNumber

抽象操作 ToNumber,負責處理非數字到數字的強制型別轉換。

Number()執行抽象操作 ToNumber,函式的轉換規則如下。

  1. 如果是 Boolean 值,true 和 false 將分別被轉換為 1 和 0。
  2. 如果是數字值,只是簡單的傳入和返回。
  3. 如果是 null 值,返回 0。
  4. 如果是 undefined,返回 NaN。
  5. 如果是字串:如果字串是空的(不包含任何字元),則將其轉換為0;如果含非數字,則將其轉換為 NaN。
  6. 如果是物件,則執行ToPrimitive抽象操作,返回基本型別再按照以上規則處理。
Number()  // 0
Number('')  // 0
Number(' ')  // 0
Number('0')  // 0
Number('asdf')  // NaN
Number(true)  // 1
Number(false)  // 0
Number(null)  // 0
Number(undefined)  // NaN    與null不同,需要注意
// 物件會先通過抽象操作ToPrimitive轉為基本型別,然後再轉數字
Number({})  // NaN
Number([])  // 0
Number([''])  // 0
Number([' '])  // 0
Number(['0'])  // 0
Number([1,2])  // NaN
複製程式碼

3. ToBoolean

抽象操作 ToBoolean,負責處理非布林值到布林值的強制型別轉換。

轉換為 boolean 型別是最為簡單的一個。轉換規則如下:

(1) 可以被強制型別轉換為 false 的值

  • undefined
  • null
  • false
  • +0、-0 和 NaN
  • ""

(2) 其他值會被被強制型別轉換為 true

這裡有一個概念需要先理解:js的操作符和運算元組成了表示式,表示式必定會返回一個值。無論是一元操作++a,還是布林操作[] || false,都會返回一個值。另外關於js運算子優先順序請參閱MDN的:運算子優先順序

一元操作符

// 假設存在變數a
+a // 一元加操作符
-a // 一元減操作符
++a // 前置遞增操作符
--a // 前置遞減操作符
a++ // 後置遞增操作符
a-- // 後置遞減操作符
複製程式碼

一元操作符指的是隻能操作一個值的操作符,區別與加性操作符可以操作兩個值(如a+b)。

1. 一元加減操作符

一元加操作符+用於非數字的強制型別轉換,作用等同於Number()。如:

+'1.1' // 1.1
+'asdf' // NaN
+true // 1
+false // 0
+null // 0
+undefined // NaN
+{} // NaN
+[] // 0
+new Date() // 1556258367546
複製程式碼

一元減操作符-行為與+類似,只不過最終運算結果是負數。如-true結果是-1

2. 遞增遞減操作符

不同於一元加減操作符,遞增遞減操作符只能作用於number型別。若用於其他型別會直接拋錯。

//前置遞增
var a = 57;
var b = ++a;
console.log(b); // 58
//後置遞增
var a = 57;
var b = a++;
console.log(b); // 57
複製程式碼

前置遞增和後置遞增的區別在於,前置遞增++a的返回值是增加1的,而後置遞增a++的返回值是不增加的。 遞減和遞增規則一樣,不再廢話。

加性操作符

1. 加法操作符+

+ 操作符常用於數學的計算和字串的拼接,規則如下:

  1. 如果兩個操作符都是數值,執行常規的加法計算
  2. 如果有一個運算元是 NaN,則結果是 NaN;
  3. 如果兩個運算元都是字串,則將第二個運算元與第一個運算元拼接起來;
  4. 如果只有一個運算元是字串,則將另一個運算元轉換為字串,然後再將兩個字串拼接起來。
  5. 如果有一個運算元是物件,則執行抽象操作ToPrimitive(先valueOf再toString)取的返回值,然後再應用前面關於字串的規則。
  6. 對於 undefined 和 null,則分別呼叫 String()函式並取得字串"undefined"和"null"。
1+1 // 2
NaN+1 // NaN
'asdf'+'ghjk' // 'asdfghjk'
1+1+'1' // '21'
[]+1 // 1
null+undefined // 'nullundefined'
[]+{}+1 // '[Object object]1'   實際執行:''+'[Object object]'+1
{}+[]+1 // 1    {}位於行首會被解釋為程式碼塊,此處程式碼塊被忽略,因此實際執行:+[]+1,結果為數字1
複製程式碼

2. 減法操作符-

  1. 如果兩個操作符都是數值,執行常規的加法計算
  2. 如果有一個運算元是 NaN,則結果是 NaN;
  3. 如果有一個運算元是字串、布林值、null或undefined,則先在後臺呼叫Number()函式將其轉換為數值,然後再根據前面的規則執行減法計算。如果轉換的結果是 NaN,則減法的結果就是 NaN;
  4. 如果有一個運算元是物件,則執行抽象操作ToPrimitive,先呼叫物件的valueOf()方法以取得表示該物件的數值。如果得到的值是NaN,則減法的結果就是NaN。如果物件沒有 valueOf()方法或者返回的不是基本型別值,則呼叫其 toString()方法並將得到的字串轉換為數值。
1-1 // 0
NaN-1 // NaN
10-true-null // 9
10-true-undefined // NaN
[]-1 // 0
['11']-11 // 0
11-{} // NaN
複製程式碼

乘性操作符

乘性操作符包括乘法*、除法/、除餘(求模)%。規則如下:

  1. 如果兩個操作符都是數值,執行常規乘除求模;
  2. 如果有一個運算元是 NaN,則結果是 NaN;
  3. 如果有一個運算元不是數值,則在後臺呼叫 Number()將其轉換為數值,然後再應用上面的規則。

數值計算較為特殊的如下:

Infinity*0 // NaN
Infinity/Infinity // NaN
0/0 // NaN
Infinity%a // NaN a為任意數值
a%0 // NaN a為任意數值
複製程式碼

布林操作符

1. 邏輯非 !

邏輯非操作符會將任意值轉換為一個布林值,轉換規則和Boolean()函式相反。連續使用兩個邏輯非操作符,等同於呼叫了Boolean()。常見有大牛寫程式碼用!!isTrue來代替Boolean(isTrue)函式。

!undefined // true
!!undefined // false
!NaN // true
!!NaN // false
!1234 // false
!!1234 // true
!'' // true
!!'' // false
複製程式碼

2. 邏輯或 ||

短路操作:如果第一個運算元能夠決定結果,那麼就不會再對第二個運算元求值。

邏輯或操作符是短路操作,如果第一個運算元的求值結果為true,則直接返回第一個運算元,不再對第二個運算元求值。如果第一個操作符求職結果為false,則返回第二個運算元。因此,常見大神寫程式碼isExist || getIsExist(),就是利用的短路操作,如果isExist求值結果為true,就不再執行getExist()函式。

[] || 0 // []   物件(包括陣列、函式等)的求值結果永遠為`true`,直接返回這個物件
0 || [] // []
1 || [] // 1
NaN || 0 // 0
複製程式碼

3. 邏輯與 &&

邏輯與操作屬於短路操作,即如果第一個運算元求值結果為false,則直接返回第一個運算元,那麼就不會再對第二個運算元求值。如果第一個運算元求值為true,則返回第二個運算元。可以用來做條件限制obj && obj.value。只有obj物件存在了,才會取obj.value值。

0 && true // 0
null && [] // null
NaN && null // NaN
[] && {} // {}
複製程式碼

相等操作費

相等操作符有== != === !==四個,其中相等和不相等實行先轉換型別再比較,全等和不全等實行僅比較而不轉換型別。相等操作符返回布林值truefalse

1. 相等和不相等

不同型別運算元比較規則如下:

  • 先判斷是否在對比 null 和 undefined,是的話就會返回 true。null和undefined不相等於其他任何值。
  • 判斷兩者型別是否為 string 和 number,是的話就會將字串轉換為 number;
  • 判斷其中一方是否為 boolean,是的話就會把 boolean 轉為 number 再進行判斷;
  • 判斷其中一方是否為 object 且另一方為 string、number 或者 symbol,是的話就會把 object 轉為原始型別再進行判斷。
[] == ![] // true
/* 首先,布林操作符!優先順序更高,所以被轉變為:[] == false
 * 其次,運算元存在布林值false,將布林值轉為數字:[] == 0
 * 再次,運算元[]是物件,轉為原始型別(先呼叫valueOf(),得到的還是[],再呼叫toString(),得到空字串''):'' == 0
 * 最後,字串和數字比較,轉為數字:0 == 0
*/
NaN == NaN // false     NaN不等於任何值
null == undefined // true
null == 0 // false
undefined == 0 // false
複製程式碼

全等和不全等

全等和不全等在比較之前不轉換型別,所以相對簡單:

null === undefined // false
'1' === 1 // false
0 === false // false
[] === [] // false    引用型別比較相等性還要看是否指向同一個記憶體地址
NaN === NaN // false    NaN比較特殊,不等於自身
複製程式碼

關係操作符

關係操作符小於(<)、大於(>)、小於等於(<=)和大於等於(>=)比較兩個值的大小,返回一個布林值。當有一個運算元是非數值時,就會發生型別轉換:

  1. 如果兩個運算元都是數值,則執行數值比較。
  2. 如果兩個運算元都是字串,則比較兩個字串對應的字元編碼值。
  3. 如果一個運算元是數值,則將另一個運算元轉換為一個數值,然後執行數值比較。
  4. 如果一個運算元是物件,則執行ToPrimitive轉為基本型別(先valueOf再toString)。
  5. 如果一個運算元是布林值,則先將其轉換為數值,然後再執行比較。
'23' <'3' // true    比較的是字元編碼值
'23' < 3 // false    執行規則3
NaN > 0 // false     NaN比較總會返回false
null >= 0 // true    執行規則3,注意null相等性比較和關係比較不一樣
undefined >= 0  //false    undefined執行關係比較會轉化為NaN,總是返回false

複製程式碼

條件操作符

1. 條件操作符

三元表示式就是由條件操作符? :組成:

a > b ? a : b;  // 如果 ? 前的操作求值為 true ,則返回 a ,否則返回 b
複製程式碼

2. 賦值操作符

js中等號 = 用於賦值操作,var a = 1就是把值1賦值給變數a。可以和 + - * / % 構成複合賦值:

a += b // 等同於 a = a + b
a -= b // 等同於 a = a - b
a *= b // 等同於 a = a * b
a /= b // 等同於 a = a / b
a %= b // 等同於 a = a % b
複製程式碼

3. 逗號操作符

逗號操作符常用於一條語句宣告多個變數:var a = 1, b = 2, c;

4. 位操作符

js中數值是以64位格式儲存的,前32位是整數,後32位是小數。位操作符會將64位的值轉為32位的,所以位操作符會強制將浮點數轉為整數。下面說幾個常用的位操作符:

  1. 位操作符 ~~x相當於-(x+1),可以用來代替indexOf作為判斷條件。~str.indexOf('asdf')相當於str.indexOf('asdf')>-1
  2. 位操作符 |:可用於將值截除為一個 32 位整數。1.11 | 0執行結果是1

總結

js的型別轉換雖然很讓人頭疼,但並不是無跡可尋。只要掌握了規則,就能夠按規則判斷出來型別到底會如何轉換。而規則中很重要的一部分是,引用型別到基本型別的轉換是通過ToPrimitive抽象操作完成的。掌握了ToPrimitive抽象操作,就掌握了型別轉換的核心規則。

型別轉換是很常見和很常用的,雖然規則多了點,但卻值得去努力學習。

相關文章