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内部在类型转换时作了哪些操做呢?算法
咱们首先来了解强制转换为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顾名思义即把其它类型转换为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
在这里特别说明一下
StrWhiteSpace:在js中StrWhiteSpace包含WhiteSpace(空白符)和LineTerminator(终止符)。
StrNumericLiteral:能够理解为包含Infinity和数字的字符串集合。
StringNumericLiteral:包含StrNumericLiteral和StrWhiteSpace的集合code
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 |
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
转换为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:
咱们在上面ToNumber和ToString方法中注意到Object类型转换为Number和String时都会调用ToPrimitive方法。该方法接受一个input输入参数和一个可选的PreferredType参数。PreferredType是用来决定当某个对象可以转换为多个基本类型时该返回什么类型。但是ToPrimitive内部到底是如何操做来返回Number或String类型的呢?若是要深刻探究其具体的操做步骤可能花大半天也不能彻底理清,里面包含了各类方法的调用以及复杂的逻辑判断还有各类安全检测,我不仔细深刻下去。我这边假设全部的判断都按正常流程走,全部安全机制都经过不报错误,那么一个对象转换为Number或String就能够归纳为如下几个判断:
@@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中强制转换时发生的过程基本捋了一遍,接下来咱们来了解一下相等操做符两边发生了什么。
ECMAScript官方对(==)操做的说法是Abstract Equality Comparison(抽象的相等比较),它对x==y定义了下面一些规则:
Strict Equality Comparison(严格的相等比较)对x===y定义下列规则:
若是x的类型是Number:
- 若是x或y是NaN,返回false。 - 若是x和y数值相同,返回true。 - 若是x是+0,y是-0,返回true。 - 若是x是-0,y是+0,返回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是一个总体,因此{}也被强制转换了。
到这里我想说的基本就结束了。若是文中有错误或者有某些强制转换的情形没有涉及到请及时留言告知,我会修改并补充进去。