原文鏈接, 閱讀時長: 10'
引子
強制類型轉換是JavaScript開發人員最頭疼的問題之一, 它常被詬病爲語言設計上的一個缺陷, 太危險, 應該束之高閣.
作爲開發人員, 往往會遇到或寫過涉及到類型轉換的代碼, 只是我們從來沒有意識到. 因爲我們基本碰運氣.
猜猜看😏:
- 作爲基本類型值, 爲什麼我們可以使用相關的屬性或方法? eg:
'hello'.charAt(0)
(內置類型和內建函數的關係) -
a && (b || c)
這波操作我們知道, 那麼if (a && (b || c))
, 這裏又做了哪些操作? (||和&&) -
if (a == 1 && a== 2) { dosomething }
, dosomething竟然執行了, 什麼鬼? (ToPrimitive) -
[] == ![]
=> true ?;false == []
=> true ?;"0" == false
=> true ?(抽象相等) -
if (~indexOf('a'))
, 這波操作熟悉不? (+/-/!/~) - 在String, Number, Boolean類型之間比較時, 進行的強制類型轉換又遵循了哪些規則? (抽象操作)
下面就要學會用實力碰運氣.
類型
內置類型
JavaScript 有七種內置類型. 空值: null, 未定義: undefined, 布爾值: boolean, 數字: number, 字符串: string, 對象: object, 符號: symbol. 除 對象:object, 爲複雜數據類型, 其它均爲基本數據類型.
內建函數
常用的內建函數: String(), Number(), Boolean(), Array(), Object(), Function(), RegExp(), Date(), Error(), Symbol().
內置類型和內建函數的關係
爲了便於操作基本類型值, JavaScript提供了封裝對象(內建函數), 它們具有各自的基本類型相應的特殊行爲. 當讀取一個基本類型值的時候, JavaScript引擎會自動對該值進行封裝(創建一個相應類型的對象包裝它)從而能夠調用一些方法和屬性操作數據. 這就解釋了 問題 1
.
類型檢測
typeof => 基本類型的檢測均有同名的與之對應. null 除外, null是假值, 也是唯一一個typeof檢測會返回 'object'
的基本數據類型值.
typeof null // "object"
let a = null;
(!a && typeof a === 'object') // true
複雜數據類型typeof檢測返回 'object'
, function(函數)除外. 函數因內部屬性[[Call]]使其可被調用, 其實屬於可調用對象.
typeof function(){} // "function"
Object.prototype.toString => 通過typeof檢測返回'object'
的對象中還可以細分爲好多種, 從內建函數就可以知道.它們都包含一個內部屬性[[Class]], 一般通過Object.prototype.toString(...)來查看.
const str = new String('hello');
const num = new Number(123);
const arr = new Array(1, 2, 3);
console.log(Object.prototype.toString.call(str))
console.log(Object.prototype.toString.call(num))
console.log(Object.prototype.toString.call(arr))
// [object String]
// [object Number]
// [object Array]
抽象操作
在數據類型轉換時, 處理不同的數據轉換都有對應的抽象操作(僅供內部使用的操作), 在這裏用到的包括 ToPrimitive, ToString, ToNumber, ToBoolean. 這些抽象操作定義了一些轉換規則, 不論是顯式強制類型轉換, 還是隱式強制類型轉換, 無一例外都遵循了這些規則(顯式和隱式的命名叫法來自《你不知道的JavaScript》). 這裏就解釋了 問題 5
和 問題 6
.
ToPrimitive
該抽象操作是將傳入的參數轉換爲非對象的數據. 當傳入的參數爲 Object 時, 它會調用內部方法[[DefaultValue]]
遵循一定規則返回非複雜數據類型, 規則詳見 DefaultValue. 故 ToString, ToNumber, ToBoolean在處理Object時, 會先經過ToPrimitive處理返回基本類型值.
[[DefaultValue]](hint)
語法:
[[DefaultValue]]
的規則會依賴於傳入的參數hint
, ToString傳入的 hint
值爲 String
, ToNumber傳入的 hint
值爲 Number
.
-
[[DefaultValue]](String)
=> 若toString
可調用, 且toString(Obj)
爲基本類型值, 則返回該基本類型值. 否則, 若valueOf
可調用, 且valueOf(Obj)
爲基本類型值, 則返回該基本類型值. 若以上處理還未得到基本類型值, 則拋出TypeError
. -
[[DefaultValue]](Number)
=> 該規則正好和上規則調用toString
,valueOf
的順序相反. 若valueOf
可調用, 且valueOf(Obj)
爲基本類型值, 則返回該基本類型值. 否則, 若toString
可調用, 且toString(Obj)
爲基本類型值, 則返回該基本類型值. 若以上處理還未得到基本類型值, 則拋出TypeError
. -
[[DefaultValue]]()
=> 未傳參時, 按照hint
值爲Number
處理.Date
對象除外, 按照hint
值爲String
處理.
現在我們就用以上的知識點來解釋 問題 3
是什麼鬼.
let i = 1;
Number.prototype.valueOf = () => {
return i++
};
let a = new Number("0"); // 字符串強制轉換爲數字類型是不執行Toprimitive抽象操作的.
console.log('a_1:', a);
if(a == 1 && a == 2) {
console.log('a==1 & a==2', 'i:', i);
}
// a==1 & a==2 i: 3
我們改寫了內建函數 Number 原型上的 valueOf
方法, 並使得一個字符串轉換成 Number 對象, 第一次 Object 類型和 Number 類型做比較時, Object 類型將進行 ToPrimitive 處理(抽象相等), 內部調用了 valueOf, 返回 2. 第二次同樣的處理方式, 返回 3.
ToString
該抽象操作負責處理非字符串到字符串的轉換.
type | result |
---|---|
null | "null" |
undefined | "undefined" |
boolean | true => "true"; false => "false" |
string | 不轉換 |
number | ToString Applied to the Number Type |
Object | 先經ToPrimitive返回基本類型值, 再遵循上述規則 |
ToNumber
該抽象操作負責處理非數字到數字的轉換.
type | result |
---|---|
null | +0 |
undefined | NaN |
boolean | true => 1; false => 0 |
string | ToNumber Applied to the String Type |
number | 不轉換 |
Object | 先經ToPrimitive返回基本類型值, 再遵循上述規則 |
常見的字符串轉換數字:
- 字符串是空的 => 轉換爲0.
- 字符串只包含數字 => 轉換爲十進制數值.
- 字符串包含有效的浮點格式 => 轉換爲對應的浮點數值.
- 字符串中包含有效的十六進制格式 => 轉換爲相同大小的十進制整數值.
- 字符串中包含除以上格式之外的符號 => 轉換爲 NaN.
ToBoolean
該抽象操作負責處理非布爾值到布爾值轉換.
type | result |
---|---|
null | false |
undefined | false |
boolean | 不轉換 |
string | "" => false; 其它 => true |
number | +0, −0, NaN => false; 其它 => true |
Object | true |
真值 & 假值
假值(強制類型轉換false的值) => undefined
, null
, false
, +0
, -0
, NaN
, ""
.
真值(強制類型轉換true的值) => 除了假值, 都是真值.
特殊的存在
假值對象 => documen.all 等. eg: Boolean(window.all)
// false
隱式強制類型轉換
+/-/!/~
-
+/- 一元運算符
=> 運算符會將操作數進行ToNumber處理. -
!
=> 會將操作數進行ToBoolean處理. -
~
=> (~x)相當於 -(x + 1) eg: ~(-1) ==> 0; ~(0) ==> 1; 在if (...)中作類型轉換時, 只有-1
時, 才爲假值. -
+加號運算符
=> 若操作數有String類型, 則都進行ToString處理, 字符串拼接. 否則進行ToNumber處理, 數字加法.
條件判斷
-
if (...)
,for(;;;)
,while(...)
,do...while(...)
中的條件判斷表達式. -
? :
中的條件判斷表達式. -
||
和&&
中的中的條件判斷表達式.
以上遵循ToBoolean規則.
||和&&
- 返回值是兩個操作數的中的一個(且僅一個). 首先對第一個操作數條件判斷, 若爲非布爾值則進行ToBoolean強制類型轉換.再條件判斷.
-
||
=> 條件判斷爲true, 則返回第一個操作數; 否則, 返回第二個操作數. 相當於 a ? a : b; -
&&
=> 條件判斷爲true, 則返回第二個操作數; 否則, 返回第一個操作數, 相當於 a ? b : a;
結合條件判斷, 解釋下問題 2
let a = true;
let b = undefined;
let c = 'hello';
if (a && (b || c)) {
dosomething()
}
a && (b || c) 返回 'hello', if語句中經Toboolean處理強制類型轉換爲true.
抽象相等
這裏的知識點是用來解釋 問題 4 的, 也是考驗人品的地方. 這下我們要靠實力拼運氣.
-
同類型的比較.
+0 == -0 // true null == null // true undefined == undefined // true NaN == NaN // false, 唯一一個非自反的值
-
null 和 undefined 的比較.
null == undefined // true undefined == null // true
- Number 類型和 String 類型的比較. => String 類型要強制類型轉換爲 Number 類型, 即 ToNumber(String) .(參見ToNumber)
- Boolean 類型和其它類型的比較. => Boolean 類型要強制類型轉換爲 Number 類型, 即 ToNumber(Boolean) .(參見ToNumber)
- Object 類型和 String 類型或 Number 類型. => Object 類型要強制轉換爲基本類型值, 即 ToPrimitive(Object) .(參見ToPrimitive)
- 其它情況, false.
回頭看看 問題 4 中的等式. [] == ![]
, false == []
, "0" == false
.[] == ![]
=> !
操作符會對操作數進行 ToBoolean 處理, []
是真值, !true
則爲 false. 再遵循第 4 點, Boolean 類型經過 ToNumber 轉換爲 Number 類型, 則爲數值 0
. 再遵循第 5 點, 對 []
進行 ToPrimitive 操作, 先後調用 valueOf()
, toString()
直到返回基本類型, 直到返回 ""
. (先[].valueOf() => [], 非基本類型值; 再[].toString() => "", 基本類型值, 返回該基本類型值.). 再遵循第 3 點, 對 ""
進行 ToNumber 處理, 則爲數值 0
. 到此, 0 == 0
, 再遵循第 1 點(其實沒寫全😌, 詳見The Abstract Equality Comparison Algorithm), return true, 完美!😏.false == []
=> 同理 [] == ![]
."0" == false
=> 同理 [] == ![]
.
[] == ![] // true
false == [] // true
"0" == false // true
運氣是留給有準備的人, 所以呢, 我要準備買彩票了.😏