深入淺出說強制類型轉換

原文鏈接, 閱讀時長: 10'

引子

強制類型轉換是JavaScript開發人員最頭疼的問題之一, 它常被詬病爲語言設計上的一個缺陷, 太危險, 應該束之高閣.

作爲開發人員, 往往會遇到或寫過涉及到類型轉換的代碼, 只是我們從來沒有意識到. 因爲我們基本碰運氣.

猜猜看😏:

  1. 作爲基本類型值, 爲什麼我們可以使用相關的屬性或方法? eg: 'hello'.charAt(0) (內置類型和內建函數的關係)
  2. a && (b || c) 這波操作我們知道, 那麼 if (a && (b || c)), 這裏又做了哪些操作? (||和&&)
  3. if (a == 1 && a== 2) { dosomething }, dosomething竟然執行了, 什麼鬼? (ToPrimitive)
  4. [] == ![] => true ?; false == [] => true ?; "0" == false => true ?(抽象相等)
  5. if (~indexOf('a')), 這波操作熟悉不? (+/-/!/~)
  6. 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.

  1. [[DefaultValue]](String) => 若 toString 可調用, 且 toString(Obj) 爲基本類型值, 則返回該基本類型值. 否則, 若 valueOf 可調用, 且 valueOf(Obj) 爲基本類型值, 則返回該基本類型值. 若以上處理還未得到基本類型值, 則拋出 TypeError.
  2. [[DefaultValue]](Number) => 該規則正好和上規則調用 toString, valueOf 的順序相反. 若 valueOf 可調用, 且 valueOf(Obj) 爲基本類型值, 則返回該基本類型值. 否則, 若 toString 可調用, 且 toString(Obj) 爲基本類型值, 則返回該基本類型值. 若以上處理還未得到基本類型值, 則拋出 TypeError.
  3. [[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返回基本類型值, 再遵循上述規則

常見的字符串轉換數字:

  1. 字符串是空的 => 轉換爲0.
  2. 字符串只包含數字 => 轉換爲十進制數值.
  3. 字符串包含有效的浮點格式 => 轉換爲對應的浮點數值.
  4. 字符串中包含有效的十六進制格式 => 轉換爲相同大小的十進制整數值.
  5. 字符串中包含除以上格式之外的符號 => 轉換爲 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


隱式強制類型轉換

+/-/!/~

  1. +/- 一元運算符 => 運算符會將操作數進行ToNumber處理.
  2. ! => 會將操作數進行ToBoolean處理.
  3. ~ => (~x)相當於 -(x + 1) eg: ~(-1) ==> 0; ~(0) ==> 1; 在if (...)中作類型轉換時, 只有-1時, 才爲假值.
  4. +加號運算符 => 若操作數有String類型, 則都進行ToString處理, 字符串拼接. 否則進行ToNumber處理, 數字加法.

條件判斷

  1. if (...), for(;;;), while(...), do...while(...)中的條件判斷表達式.
  2. ? : 中的條件判斷表達式.
  3. ||&& 中的中的條件判斷表達式.

以上遵循ToBoolean規則.

||和&&

  1. 返回值是兩個操作數的中的一個(且僅一個). 首先對第一個操作數條件判斷, 若爲非布爾值則進行ToBoolean強制類型轉換.再條件判斷.
  2. || => 條件判斷爲true, 則返回第一個操作數; 否則, 返回第二個操作數. 相當於 a ? a : b;
  3. && => 條件判斷爲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 的, 也是考驗人品的地方. 這下我們要靠實力拼運氣.

  1. 同類型的比較.

        +0 == -0 // true
        null == null // true
        undefined == undefined // true
        NaN == NaN // false, 唯一一個非自反的值
  2. nullundefined 的比較.

        null == undefined // true
        undefined == null // true
  3. Number 類型和 String 類型的比較. => String 類型要強制類型轉換爲 Number 類型, 即 ToNumber(String) .(參見ToNumber)
  4. Boolean 類型和其它類型的比較. => Boolean 類型要強制類型轉換爲 Number 類型, 即 ToNumber(Boolean) .(參見ToNumber)
  5. Object 類型和 String 類型或 Number 類型. => Object 類型要強制轉換爲基本類型值, 即 ToPrimitive(Object) .(參見ToPrimitive)
  6. 其它情況, 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

運氣是留給有準備的人, 所以呢, 我要準備買彩票了.😏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章