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中有关类型转换的知识。

类型转换

学过js的应该都了解js是一门弱类型语言。你在声明一个变量的时候没有告诉它是什么类型,于是在程序运行时,你可能不知不觉中就更改了变量的类型。可能有些是你故意改的,另一些可能并不是你的本意,但是不管怎样你都不可避免的会遇到类型转换(强制或隐含)。让我们看一下下面的列子:

  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官方也专门给出了转换规则:

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字面量值的算法相似,但是要注意一下细节:

  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的集合

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”)中分类的所有代码点。

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是一个整体,所以{}也被强制转换了。

结束

到这里我想说的基本就结束了。如果文中有错误或者有某些强制转换的情形没有涉及到请及时留言告知,我会修改并补充进去。


fanqifeng
536 声望23 粉丝

javascript语言爱好者,喜欢发掘js中的特异之处。现阶段沉迷购书无法自拔。