有關javascript強制轉換不得不說的故事

javascript是一門很是奇特的語言,它有時候奇特的會讓人懷疑人生。好比讓咱們看一下下面的一些奇葩例子:javascript

false == '0'           //true   '哇'
 false == 0             //true   '哦'
 false == ''            //true   '噢'
 false == []            //true   '啥?'
 
 0 == ''                //true   'what?'
 0 == []                //true   
 0 == '0'               //true   
 [] == '0'              //false  'why?'
 [] == ''               //true   
 
 //-----------更驚訝的是---------------

 [] == ![]              //true    'WTF!'
 [2] == 2               //true  
 '' == [null]           //true 
 0 == '\n'              //true     我還能說什麼呢?
 false == '\n'          //true

還有許多能夠列出來嚇你一跳的例子,別懷疑我是隨便編出來騙你的。當時我在瀏覽器運行這些時,我都懷疑我之前學得是假的js。若是要形容我當時的表情的話,你想一下黑人小哥的表情就能明白我當時是有多懷疑人生。
好,如今讓咱們先喝杯水壓壓驚,暫時忘記前面那些奇葩的例子。咱們首先了解一下js中有關類型轉換的知識。html

類型轉換

學過js的應該都瞭解js是一門弱類型語言。你在聲明一個變量的時候沒有告訴它是什麼類型,因而在程序運行時,你可能不知不覺中就更改了變量的類型。可能有些是你故意改的,另外一些可能並非你的本意,可是無論怎樣你都不可避免的會遇到類型轉換(強制或隱含)。讓咱們看一下下面的列子:java

var a = '1';
  var b= Number(a);               // b=1; 
  +a;                             // 1;
  b + '';                         // '1';

你們應該都知道答案,不少人在代碼中或多或少都會用到這些方法,而且都明白其中發生了值的類型轉換,可是大家是否有深刻了解js內部在類型轉換時作了哪些操做呢?算法

ToBoolean(argument)

咱們首先來了解強制轉換爲Boolean類型時,發生了什麼操做。在用調用Boolean(a)或者!a等操做將值轉換爲Boolean類型時,js內部會調用ToBoolean方法來進行轉換,該方法定義瞭如下規則:瀏覽器

argument的類型 轉換的結果
Undefined false
Null false
Boolean argument
Number 若是argument是 +0、-0、NaN, 返回false; 不然返回true.
String 若是arguments是空字符串(長度爲0)返回false,不然返回true
Object true
Symbol(ES6新增類型) true

從這個列表中咱們簡單歸納一下就是隻要argument的值是(undefined、null、+0、-0、NaN、''(空字符串)以及false))這7個裏的其中一個,那轉換以後返回的是false,其餘都爲true。js專門把這7個值放到一個falsy列表中,其他值都放在truthy列表。安全

ToNumber(argument)

ToNumber顧名思義即把其它類型轉換爲Number類型(js內部調用的方法,外部沒法訪問到),ECMAScript官方也專門給出了轉換規則:app

argument的類型 轉換的結果
Undefined NaN
Null +0
Boolean false爲+0,true爲1
Number 返回argument
Object 執行如下步驟:讓primValue成爲ToPrimitive(argument, hint Number)的返回值,再調用ToNumber(primValue)返回。
Symbol(ES6新增類型) 拋出TypeError異常.

從列表能夠明顯看到少了一個String類型轉換爲Number的規則。由於String轉Number,js內部有很是複雜的判斷,我這裏面不詳細說轉換的細節,有興趣的能夠看一ECMAScript官方的說明。只要知道它與肯定Number字面量值的算法類似,可是要注意一下細節:prototype

  1. 一個空(empty)的或只包含空格的字符串被轉換爲+0。
  2. StrWhiteSpace會轉化爲+0
  3. StrNumericLiteral先後的StrWhiteSpace會被忽略。
  4. StrNumericLiteral前面的多個0會被忽略。
  5. 不是StringNumericLiteral的擴展會變爲NaN。

在這裏特別說明一下
StrWhiteSpace:在js中StrWhiteSpace包含WhiteSpace(空白符)和LineTerminator(終止符)。
StrNumericLiteral:能夠理解爲包含Infinity和數字的字符串集合。
StringNumericLiteral:包含StrNumericLiteral和StrWhiteSpace的集合code

WhiteSpace

Unicode Code Point name
U+0009 製表符<TAB>
U+000B 垂直方向的製表符<VT>
U+000C 換頁符<FF>
U+0020 空格符<SP>
U+00A0 不換行空格符 <NBSP>
U+FEFF 零寬度不換行空格符 <ZWNBSP>
其餘種類的「Zs」(分隔符,空白) Unicode 「Space_Separator」<USP>

ECMAScript WhiteSpace有意排除具備Unicode「White_Space」屬性但未在類別「Space_Separator」(「Zs」)中分類的全部代碼點。htm

Zs列表
我這邊列出了Unicode其它「Zs」的列表,感興趣的能夠了解一下:

Unicode Code Point name
U+1680 OGHAM SPACE MARK
U+180E MONGOLIAN VOWEL SEPARATOR
U+2000 EN QUAD
U+2001 EM QUAD
U+2002 EN SPACE
U+2003 EM SPACE
U+2004 THREE-PER-EM SPACE
U+2005 FOUR-PER-EM SPACE
U+2006 SIX-PER-EM SPACE
U+2007 FIGURE SPACE
U+2008 PUNCTUATION SPACE
U+2009 THIN SPACE
U+200A NARROW NO-BREAK SPACE
U+202F FIGURE SPACE
U+205F MEDIUM MATHEMATICAL SPACE  
U+3000 IDEOGRAPHIC SPACE

LineTerminator

Unicode Code Point name
U+000A 換行符<LF>
U+000D 回車<CR>
U+2028 行分隔符<LS>
U+2029 段分隔符<PS>

上面的過程說的很抽象,不是很容易理解,咱們來看一下具體的列子:

Number('');                   //0  empty
Number('    ');               //0  多個空格
Number('\u0009');             //0  製表符也能夠用Number('\t')表示
Number(
);                            //0  換行符也能夠用Number('\n')或Number('\u000A')表示
Number('000010');             //10 1前面的多個0被忽略
Number('    10    ');         //10 string先後多個StrWhiteSpace
Number('\u000910\u0009');     //10 string先後有製表符
Number('ab');                 //NaN

StrNumericLiteral中的其它進制的數字與十進制有類似的規則,但轉化的Number值是十進制下的值:

Number('0b10');               //2   (二進制)
Number('0o17');               //15  (八進制)
Number('0xA');                //10  (十六進制)

還有說明一點是十進制下數字的科學計數法顯示的字符串也能經過ToNumber轉換爲Number類型:

Number('1.2e+21');            //1.2e+21
Number('1.2e-21');            //1.2e-21

ToString(argument)

轉換爲String類型的規則以下:

argument的類型 轉換的結果
Undefined 'undefined'
Null 'null'
Boolean false爲'false',true爲true'
String argument
Object 執行如下步驟:讓primValue成爲ToPrimitive(argument, hint String)的返回值,再調用ToString(primValue)返回。
Symbol(ES6新增類型) 拋出TypeError異常.

一樣的在表中我也沒有列出Number類型轉換爲String類型的規則,Number轉String並非簡單的在數字先後加上‘或「就好了(即便看起來是這樣),裏面涉及到了複雜的數學算法,我不細說(好吧主要是我沒有特別理解,具體算法能夠看文檔),在這裏我只列出幾種特殊狀況:

假設Number的值爲m:

  1. 若是m是NaN,返回String "NaN"。
  2. 若是m是+0或-0,返回String "0"。
  3. 若是m小於0, 返回字符串鏈接符"-"和ToString(-m)。
  4. 若是m是+∞,返回String "Infinity"。

ToPrimitive(input [ , PreferredType ])

咱們在上面ToNumber和ToString方法中注意到Object類型轉換爲Number和String時都會調用ToPrimitive方法。該方法接受一個input輸入參數和一個可選的PreferredType參數。PreferredType是用來決定當某個對象可以轉換爲多個基本類型時該返回什麼類型。但是ToPrimitive內部到底是如何操做來返回Number或String類型的呢?若是要深刻探究其具體的操做步驟可能花大半天也不能徹底理清,裏面包含了各類方法的調用以及複雜的邏輯判斷還有各類安全檢測,我不仔細深刻下去。我這邊假設全部的判斷都按正常流程走,全部安全機制都經過不報錯誤,那麼一個對象轉換爲Number或String就能夠歸納爲如下幾個判斷:

  1. 一個對象上是否有@@toPrimitive方法定義,若是有調用該方法返回結果。
  2. 對象上若是沒有定義@@toPrimitive方法,則沿着該對象的原型鏈向上查找,直到找到或者[[Prototype]]爲空。
  3. 若是該對象和其原型鏈上都沒有定義@@toPrimitive方法,則調用OrdinaryToPrimitive(O,hint);
  4. hint有PreferredType決定,若是PreferredType是hint Number,hint爲'number',PreferredType是hint String,hint爲'string',若是沒定義,默認hint爲'number',O就是input對象。
  5. OrdinaryToPrimitive方法的判斷是:若是hint爲'string',在O上調用« "toString", "valueOf" »。意思是在O以及原型鏈上先查找"toString"方法,找到第一個toString方法就調用toString返回結果,若是沒有就查找」valueOf「方法來返回結果。
  6. 若是hint爲'number',在O上調用« "valueOf", "toString" »。
  7. @@toPrimitive、« "toString", "valueOf" »和« "valueOf", "toString" »方法調用返回一個Object類型時可能會報TypeError錯誤

@@toPrimitive是Symbol類型,是Symbol.toPrimitive的簡寫,ES6以前沒有Symbol類型,因此只需判斷toString和valueOf方法。

我這邊用幾個例子來解釋ToPrimitive的運行過程

var a = {
    [Symbol.toPrimitive]: (hint)=>{
        if(hint==='number'){
            return 1;
        }else if(hint==='string'){
            return 'Symbol.toPrimitive';
        }else if(hint==='default'){
            return 2;
        }else{
            throw TypeError('不能轉換爲String和Number以外的類型值');        //防止內部出現錯誤     
        }
    },
    toString: () => 'toString',
    valueOf: () => 3
}; 
Number(a);                    //1         hint爲'number'
String(a);                    //'Symbol.toPrimitive'  hint爲'string'
a + '1';                      //'21'      a在進行+操做符時hint爲'default',由於程序不知道你是作字符串相加仍是數值相加
a + 1;                        //3 
+a;                           //1         此時hint爲'number',爲何hint不是'default',+a實際上內部進行ToNumber轉換,-、*、/操做符相似  

//刪除a中Symbol.toPrimitive屬性                            
delete a[Symbol.toPrimitive];
Number(a);                    //3         調用valueOf方法
String(a);                    //'toString'   調用toString方法
a + 1;                        //4        結果不是'toString1'是由於js內部先判斷valueOf方法
//刪除a中valueOf屬方法 
delete a['valueOf'];
Number(a);                    //NaN     返回的'toString'不能轉換爲有效數字
String(a);                    //'toString'
1 + a;                        //'1toString'
//重寫a中的toString方法
a.toString = () = > a;        //返回了a對象
Number(a);                    //TypeError
String(a);                    //TypeError
1 + a;                        //TypeError

上面例子看出Object類型在轉換爲String和Number時有可能會出現各類各樣的狀況。爲此咱們最好永遠不要重寫對象中的valueOf或者toString方法,以防出現意想不到的結果,若是你重寫了方法那麼你就要格外當心了。

Object.prototype.toString= () => 1;
1 + {};        //2   看到了嗎?永遠不要重寫Object中的內置方法,最好也不要在子對象中覆蓋Object的內置方法。

在此咱們對js中強制轉換時發生的過程基本捋了一遍,接下來咱們來了解一下相等操做符兩邊發生了什麼。

Abstract Equality Comparison

ECMAScript官方對(==)操做的說法是Abstract Equality Comparison(抽象的相等比較),它對x==y定義了下面一些規則:

  1. 若是x和y是同一類型,進行Strict Equality Comparison x === y。
  2. 若是x是null,y是undefined,返回true。
  3. 若是x是undefined,y是null,返回true。
  4. 若是x的類型是Number,y的類型是String,進行x==ToNumber(y)。
  5. 若是x的類型是String,y的類型是Number,進行ToNumber(x)==y。
  6. 若是x的類型是Boolean,進行ToNumber(x)==y。
  7. 若是y的類型是Boolean,進行x==ToNumber(y)。
  8. 若是x的類型是String、Number或者Symbol,y的類型是Object,進行x==ToPrimitive(y)。
  9. 若是x的類型是Object,y的類型是String、Number或者Symbol,進行ToPrimitive(x)==y。
  10. 其餘返回false

Strict Equality Comparison

Strict Equality Comparison(嚴格的相等比較)對x===y定義下列規則:

  1. 若是x和y是否是同一類型, 返回false。
  2. 若是x的類型是Number:

    - 若是x或y是NaN,返回false。
    - 若是x和y數值相同,返回true。
    - 若是x是+0,y是-0,返回true。
    - 若是x是-0,y是+0,返回true。
    - 其餘返回false。
  3. 若是x是Undefined類型,返回true。
  4. 若是x是Null類型,返回true。
  5. 若是x是String類型,x和y是徹底相同的代碼單元序列返回true,不然false。
  6. 若是x是Boolean類型,x和y都是true或都是false,返回true,不然返回false。
  7. 若是x是Symbol類型,x和y是相同的Symbol值,返回true,不然返回false。
  8. 若是x和y是相同的對象,返回true,不然返回false。

提到(===)操做符,咱們不等不說一個方法Object.is(a,b),該方法也是比較兩個值是否同樣,但它比(===)更嚴格。它們之間的區別在於若是x和y是NaN,返回true。若是x是+0,y是-0,返回false,若是x是-0,y是+0,返回false。

驗證

到這裏類型轉換和相等比較的介紹就告一段落了,如今咱們從新回過頭去看一下最開始的幾個奇特例子,你會發現它們之間的關係比較是如此的正常。我就拿([] == ![])進行講解,按照操做符優先級比較,先運行![],它的值爲false,這時等式變成([] == false);按(==)的規則7對false進行ToNumber操做,值變爲0,這時等式變爲([] == 0);按(==)的規則9對[]進行ToPrimitive操做,調用Array上的toString方法,返回'',這時等式變爲('' == 0);按(==)的規則5對''進行ToNumber操做,值變爲0,這時等式是(0==0)。咱們最終得出結論([] == ![])是對的。

補充

咱們看一下下面的例子:

1 + {};               //'1[object object]'
{} + 1;               //1
({} + 1);             //'[object object]1'

咱們發現第一和第三個表達式按照咱們預期的值輸出了,可是第二個表達式卻沒有。這裏要強調一點:第二個表達式沒有涉及到強制類型轉換。他把這個表達式當作了兩個,一個是塊{},還有一個是+1,把{}丟棄l,因此輸出的值1。至於1+{},js把他當作一個表達式,因此{}被強制轉換爲'[object object]';第三個表達式加了(),使js認爲{}+1是一個總體,因此{}也被強制轉換了。

結束

到這裏我想說的基本就結束了。若是文中有錯誤或者有某些強制轉換的情形沒有涉及到請及時留言告知,我會修改並補充進去。

相關文章
相關標籤/搜索