深入淺出說強制型別轉換

夜暁宸發表於2019-02-16

原文連結, 閱讀時長: 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

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

相關文章