JavaScript強制型別轉換的抽象操作
強制型別轉換是JavaScript/">JavaScript開發人員最頭疼的問題之一, 它常被詬病為語言設計上的一個缺陷, 太危險, 應該束之高閣.
作為開發人員, 往往會遇到或寫過涉及到型別轉換的程式碼, 只是我們從來沒有意識到. 因為我們基本碰運氣.
猜猜看:smirk::
-
作為基本型別值, 為什麼我們可以使用相關的屬性或方法? 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》強制型別轉換部分為大綱, 並參照ofollow,noindex">
ecma
規範
撰寫。)
型別
內建型別
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
(詳見ecma
規範). 這些抽象操作定義了一些轉換規則, 不論是顯式強制型別轉換, 還是隱式強制型別轉換, 無一例外都遵循了這些規則. 這裡就解釋了
問題 5
和
問題 6
.
ToPrimitive
該抽象操作是將傳入的引數轉換為非物件的資料. 當傳入的引數為 Object 時, 它會呼叫內部方法[[DefaultValue]]
遵循一定規則返回非複雜資料型別, 規則
詳見(ecma
規範: 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 |
(詳見ecma
規範: ToString Applied to the Number Type) |
Object | 先經 ToPrimitive 返回基本型別值, 再遵循上述規則 |
ToNumber
該抽象操作負責處理非數字到數字的轉換.
type | result |
---|---|
null | +0 |
undefined | NaN |
boolean | true => 1; false => 0 |
string |
(詳見ecma
規範: 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
點(其實沒寫全:relieved:, 詳見(詳見ecma
規範: The Abstract Equality Comparison Algorithm)), return true, 完美!:smirk:.false == []
=> 同理[] == ![]
."0" == false
=> 同理[] == ![]
.
[] == ![]// true false == [] // true "0" == false// true 複製程式碼
運氣是留給有準備的人, 所以呢, 我要準備買彩票了.:smirk: