[] == ![]發生了什麼?

Ghh發表於2019-02-21

記不清在某處看見了這一比較,當時對強制轉換這塊理解的還沒有特別清晰,故有此一文。以為我會以標題的表示式來展開?那你就錯了,下面直接上[] == ![]是如何轉換的:

  1. 因為!運算子的優先順序比較高,所以表示式右側先執行![],得出false,表示式變為[] == false
  2. 強制將false轉換為0,表示式變為[] == 0
  3. 將[]強制轉換為原始型別後為"",表示式變為"" == 0
  4. 將""轉換為Number型別,表示式變為0 == 0
  5. 兩側型別相同,直接返回0 === 0的結果true

前言

本文旨在總結js中強制轉換的規則及觸發強制轉換的幾種場景。ES6標準中定義了六種原始型別,分別是Undefined,Null,String,Number,Boolean,Symbol。本文中的強制轉換指的是在程式碼執行時,觸發了數值的隱式轉換,而不是程式碼顯示的指定轉換操作。

原始型別間強制轉換

發生在原始型別之間的轉換,以個人的理解是其他型別轉換為String,Number或者Boolean型別。

轉換為String型別

其他原始型別轉換為String型別通常發生在+兩邊存在字串時,會將+另一邊的值轉換為String型別。 考慮如下程式碼:

var strAddNum = "test" + 1;
var numAddStr = 1 + "test";
var boolAddStr = true + "test";
var undAddStr = undefined + "";
var nullAddStr = null + "";
console.log(strAddNum);
console.log(numAddStr);
console.log(boolAddStr);
console.log(undAddStr);
console.log(nullAddStr);
複製程式碼

程式碼傳送門,以上程式碼的執行結果均為字串。其他原始型別轉換為String型別基本是其值的字串形式,具體如下:

  • Undefined,"undefined"
  • Null,"null"
  • Boolean,"true"或"false"
  • Number,值為NaN,"NaN"
  • Number,值為+0或-0,"0"
  • Number,值為+Infinity,"Infinity"
  • Number,值為-Infinity,"-Infinity"

Number轉為字串具體可參考ES2018 7.1.12.1章節

注意:Symbol型別無法轉換為String型別。

轉換為Number型別

轉換為Number型別的情況,+-*/%等運算中,除了+之外其他運算均會轉換成Number型別,+運算時需要滿足兩側未出現String型別,該值才會被轉換為Number型別。+運算時情況較為複雜,後面會專門描述其相關轉換規則。考慮如下程式碼:

var trueAddTrue = true + true;
var trueAddFalse = true + false;
var trueAdda0 = true + 0;
var nullAddTrue = null + true;
var undefinedAdd0 = undefined + 0;
var strAdd0 = "" + 0;
console.log(trueAddTrue);
console.log(trueAddFalse);
console.log(trueAdda0);
console.log(nullAddTrue);
console.log(undefinedAdd0);
console.log(strAdd0);
複製程式碼

程式碼傳送門,在執行程式碼之前可以先考慮下以上程式碼答列印的結果分別是什麼?然後再執行,看是否符合你的預期。其他原始型別轉換為Number型別的具體如下:

  • Undefined,NaN
  • Null, +0
  • Boolaen,值為true,1
  • Boolean,值為false,+0
  • String,不可轉為Number的,NaN
  • String,可轉為Number的就是其對應的Number值(具體可參考ES2018 7.1.3.1

注意:Symbol型別同樣無法轉換為Number型別。

轉換為Boolean型別

轉換為Boolean型別的情況較為簡單,除了以下情況轉換為Boolean型別會是false,其他情況均是true

  • Undefined
  • Null
  • Number,+0,-0,NaN
  • String,長度為0的字串 這幾種false的情況在ES標準中有明確規定7.1.2

物件強制轉換為原始型別

ES中將物件轉換為原始型別的演算法,大致可描述為三種情形:

  1. 如果該物件設定了[Symbol.toPrimitive],呼叫該函式,如果其返回值為非Object型別則返回結果,否則丟擲TypeError異常
  2. 若未指定轉換提示則轉換提示為"default"
  3. 若轉換提示為"default",則將其置為"number"
  4. 當指定轉換提示為"number"時先呼叫該物件的valueOf函式並判斷其結果,如果是原始型別則返回結果,否則呼叫該物件的toString函式並判斷其返回結果,如果結果為原始型別則返回,否則丟擲異常TypeError
  5. 當指定轉換提示為"string"時先呼叫toString函式並判斷其返回結果,如果是原始型別則返回結果,否則呼叫該物件的valueOf函式並判斷其返回結果,如果結果為原始型別則返回,否則丟擲異常TypeError

上述三種情形中第一種情形優先順序最高,第二三種情形優先順序並列,具體需要根據使用場景判斷是哪一種。其中的指定轉換提示是ES標準內部呼叫該演算法時指定的。

第一種情形只有Symbol物件和Date物件內建了[Symbol.toPrimitive],且該屬性的writeable為false,enumerable為false,configurable為true 物件轉換為原始型別時發生的強制轉換非特殊情況均為第二種,第三種情況較為少見。在正常編碼工作中應該使用第二種情形就夠用了,第三種情形幾乎不會出現,要了解更多細節可查閱ES標準。

var test = {
  [Symbol.toPrimitive]: function(hint) {
     console.log(hint)
  },
  valueOf: function() {
      console.log("valueOf")
  },
  toString: function() {
      console.log("toString")
  }
}
test + "";  //"default"
test * 0;   //"number"
String(test);   //"string"
複製程式碼

程式碼傳送門上述程式碼指定了分別指定了test物件的[Symbol.toPrimitive],valueOf和toString函式,可以觀察到並valueoOf和toString函式均未被呼叫,指定的[Symbol.toPrimitive]函式可以接受一個提示引數,這個引數就是強制轉換時的強制轉換提示。這樣我們在函式中就可以根據轉換場景的不同分別返回不同的值。

原始型別強制轉換為物件(裝箱)

在開始描述這個問題之前,可以先思考一下,都有哪些場景會是強制的將原始型別轉換為物件,其實這種場景幾乎在js程式碼中隨處可見,考慮如下程式碼:

var str = "testString";
str.replace("test", "");
複製程式碼

如上程式碼中定義的str的值並不是一個物件而是一個原始型別String,原始型別顯然是沒有方法可以呼叫的。

實際上這裡的str在執行str.replace時str其值會被強制轉換為物件,得到一個String型別的例項物件,而該例項的原型上定義了一系列方法,且該例項是無法被獲取的,在執行完這行程式碼後,該例項就會被回收,所以這裡的str依然是一個字串。

考慮如下程式碼:

var a = 3;
a.fn = function(){};
a.fn();
複製程式碼

強制轉換的幾種場景

在js程式碼中會出現強制轉換的場景通常有三種:

  • +運算
  • -,*,/,%運算
  • ==比較
  • 作為判斷條件

+運算

一元+運算

做一元+運算時,均會被強制轉為Number型別,例如

var a = {
    [Symbol.toPrimitive]: function(hint) {
        console.log(hint);  // number
        if(hint === "number") {
            return 2;
        } else {
            return 9;
        }
    }
};
console.log(+a);   // 2

var b = "3";
console.log(+b);    // 3
複製程式碼

程式碼傳送門

二元+運算

二元+運算為幾種強制轉換中複雜度僅次於==比較的一種情形,個人總結其轉換步驟如下:

  1. 先將兩側數值強制轉換為原始型別(未指定轉換提示,即轉換提示為hint default);
  2. 若兩側存在String型別,均轉換為String型別,則返回兩側拼接的字串;
  3. 若第2未返回,則兩側數值強制轉換為Number型別,返回計算結果;
var a = "";
var b = {
    [Symbol.toPrimitive]: function(hint) {
        console.log(hint);  // "default"
        if(hint === "default") {
            return 2;
        } else {
            return 9;
        }
    }
};
var c = a + b;  //這裡b轉換為原始型別返回的是Number型別2,由於a是"",所以b被轉換為"2",後與""拼接返回"2"
console.log(c); // "2"

var d = 3;
var e = {
    [Symbol.toPrimitive]: function(hint) {
        console.log(hint);  // "default"
        if(hint === "default") {
            return 2;
        } else {
            return 9;
        }
    }
};
var f = d + e;  //這裡e轉換為原始型別返回的是Number型別2,由於兩側均沒有String型別,則至第3步,強制轉換為Number後返回兩側相加的結果5
console.log(f); // 5

複製程式碼

程式碼傳送門

-,*,/,%運算

這幾個運算子這涉及的強制轉換都是轉換為Number型別的,所以這裡只要搞清楚轉換為Number是怎樣的過程就可以了。上文中已經對原始型別轉換為Number型別做了描述,這裡補充一下Object轉換為Number的過程:

  1. 將物件轉換為原始型別,且轉換時會指定轉換提示為"number";
  2. 轉換為原始型別後再根據原始型別轉換為Number型別進行轉換;
var a = 8;
var b = {
    [Symbol.toPrimitive]: function(hint) {
        console.log(hint);  // "number"
        if(hint === "number") {
            return 2;
        } else {
            return 9;
        }
    }
};
console.log(a-b);   //  6
console.log(a/b);   // 4
console.log(a*b);   // 16
console.log(a%b);   // 0

console.log(undefined * 0);   //NaN
console.log(null * -1); // 0
console.log(false * -1);    //0 
console.log(true * -1); // -1
console.log("1" * -1);  // -1
複製程式碼

程式碼傳送門

==比較

==比較的基礎===比較

x === y,其具體比較步驟如下:

  1. 若x和y的型別不一致,返回false;
  2. 若x和y為Number型別,則若x和y中有一個為NaN返回false;若x和y的值相等則返回true;若x是+0,y是-0或者x是-0,y是+0則返回true;其他情況返回false;
  3. 若x和y為Undefined型別,返回true
  4. 若x和y為Null型別,返回true
  5. 若x和y為String型別,其值相同則返回true,否則返回false
  6. 若x和y為Boolean型別,其值均為true或均為false返回true,否則返回false
  7. 若x和y為Symbol型別,其值為同一個Symbol值則返回true,否則返回false
  8. 若x和y為Object型別,其值為同一個物件(其引用地址相同)則返回true,否則返回false

x == y規則

==比較的轉換規則雖然稍微多一點,實際上也就幾條規則,兩側的數值型別符合哪種就按哪種去轉換,只不過有的可能需要轉兩次,具體如下:

  1. 如果兩側型別相等則直接返回===的結果;
  2. 若x為undefined和y為null或x為null和y為undefined,返回true
  3. 若兩側為String型別和Number型別,將String型別轉換為Number型別,繼續用==比較
  4. 若有一側存在Boolean型別,將Boolean型別轉換為Number型別,繼續用==比較
  5. 若兩側為String,Number或Symbol型別和Object型別,將Object型別轉換原始型別,繼續用==比較
  6. 其他返回false

下面列舉一些可能有點違反直覺的比較

"0" == false; // true
false == 0; // true
false == ""; // true
false == []; // true
"" == 0; // true
"" == []; // true
0 == []; // true
[] == ![];  //true
複製程式碼

作為條件判斷

這種情形到沒有太多可說的,基本上就是,除了undefined,null,+0,-0,NaN,""這六個值會被轉為false,其他情況均為true;

出現將不是Boolean型別的值強制轉換的情況為

  1. if(...)
  2. for(...;...;...)第二個條件表示式
  3. while(...)和do...while(...)中的條件表示式
  4. ...?...:...三元表示式中的第一個條件表示式
  5. ||和&&

結論

其實上面描述了這麼多,日常開發環境中用到比較多的應該是作為判斷條件,+,==這三種情況了,這三種中最常見的應該是判斷條件的情況了,這種情況反而是最簡單的一種了。

相關文章