mqyqingfeng / Blog

冴羽写博客的地方,预计写四个系列:JavaScript深入系列、JavaScript专题系列、ES6系列、React系列。

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

JavaScript深入之头疼的类型转换(下)

mqyqingfeng opened this issue · comments

前言

举个例子:

console.log(1 + '1')

在 JavaScript 中,这是完全可以运行的,不过你有没有好奇,为什么 1 和 '1' 分属不同的数据类型,为什么就可以进行运算呢?

这其实是因为 JavaScript 自动的将数据类型进行了转换,我们通常称为隐式类型转换。但是我们都知道,+运算符既可以用于数字加法,也能用于字符串拼接,那在这个例子中,是将数字 1 转成字符串 '1',进行拼接运算?还是将字符串 '1' 转成数字 1,进行加法运算呢?

先卖个关子,虽然估计你也知道答案。今天,我们就常见的隐式类型转化的场景进行介绍。

一元操作符 +

console.log(+'1');

当 + 运算符作为一元操作符的时候,查看 ES5规范1.4.6,会调用 ToNumber 处理该值,相当于 Number('1'),最终结果返回数字 1

那么下面的这些结果呢?

console.log(+[]);
console.log(+['1']);
console.log(+['1', '2', '3']);
console.log(+{});

既然是调用 ToNumber 方法,回想《JavaScript 深入之头疼的类型转换(上)》中的内容,当输入的值是对象的时候,先调用 ToPrimitive(input, Number) 方法,执行的步骤是:

  1. 如果 obj 为基本类型,直接返回
  2. 否则,调用 valueOf 方法,如果返回一个原始值,则 JavaScript 将其返回。
  3. 否则,调用 toString 方法,如果返回一个原始值,则JavaScript 将其返回。
  4. 否则,JavaScript 抛出一个类型错误异常。

+[] 为例,[] 调用 valueOf 方法,返回一个空数组,因为不是原始值,调用 toString 方法,返回 ""

得到返回值后,然后再调用 ToNumber 方法,"" 对应的返回值是 0,所以最终返回 0

剩下的例子以此类推。结果是:

console.log(+['1']); // 1
console.log(+['1', '2', '3']); // NaN
console.log(+{}); // NaN

二元操作符 +

规范

现在 + 运算符又变成了二元操作符,毕竟它也是加减乘除中的加号

1 + '1' 我们知道答案是 '11',那 null + 1[] + [][] + {}{} + {} 呢?

如果要了解这些运算的结果,不可避免的我们要从规范下手。

规范地址:http://es5.github.io/#x11.6.1

不过这次就不直接大段大段的引用规范了,直接给大家讲简化后的内容。

到底当执行 + 运算的时候,会执行怎样的步骤呢?让我们根据规范11.6.1 来捋一捋:

当计算 value1 + value2时:

  1. lprim = ToPrimitive(value1)
  2. rprim = ToPrimitive(value2)
  3. 如果 lprim 是字符串或者 rprim 是字符串,那么返回 ToString(lprim) 和 ToString(rprim)的拼接结果
  4. 返回 ToNumber(lprim) 和 ToNumber(rprim)的运算结果

规范的内容就这样结束了。没有什么新的内容,ToStringToNumberToPrimitive都是在《JavaScript 深入之头疼的类型转换(上)》中讲到过的内容,所以我们直接进分析阶段:

让我们来举几个例子:

1.Null 与数字

console.log(null + 1);

按照规范的步骤进行分析:

  1. lprim = ToPrimitive(null) 因为null是基本类型,直接返回,所以 lprim = null
  2. rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = null
  3. lprim 和 rprim 都不是字符串
  4. 返回 ToNumber(null) 和 ToNumber(1) 的运算结果

接下来:

ToNumber(null) 的结果为0,(回想上篇 Number(null)),ToNumber(1) 的结果为 1

所以,null + 1 相当于 0 + 1,最终的结果为数字 1

这个还算简单,看些稍微复杂的:

2.数组与数组

console.log([] + []);

依然按照规范:

  1. lprim = ToPrimitive([]),[]是数组,相当于ToPrimitive([], Number),先调用valueOf方法,返回对象本身,因为不是原始值,调用toString方法,返回空字符串""
  2. rprim类似。
  3. lprim和rprim都是字符串,执行拼接操作

所以,[] + []相当于 "" + "",最终的结果是空字符串""

看个更复杂的:

3.数组与对象

// 两者结果一致
console.log([] + {});
console.log({} + []);

按照规范:

  1. lprim = ToPrimitive([]),lprim = ""
  2. rprim = ToPrimitive({}),相当于调用 ToPrimitive({}, Number),先调用 valueOf 方法,返回对象本身,因为不是原始值,调用 toString 方法,返回 "[object Object]"
  3. lprim 和 rprim 都是字符串,执行拼接操作

所以,[] + {} 相当于 "" + "[object Object]",最终的结果是 "[object Object]"。

下面的例子,可以按照示例类推出结果:

console.log(1 + true);
console.log({} + {});
console.log(new Date(2017, 04, 21) + 1) // 这个知道是数字还是字符串类型就行

结果是:

console.log(1 + true); // 2
console.log({} + {}); // "[object Object][object Object]"
console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"

注意

以上的运算都是在 console.log 中进行,如果你直接在 Chrome 或者 Firebug 开发工具中的命令行直接输入,你也许会惊讶的看到一些结果的不同,比如:

type1

我们刚才才说过 {} + [] 的结果是 "[object Object]" 呐,这怎么变成了 0 了?

不急,我们尝试着加一个括号:

type2

结果又变成了正确的值,这是为什么呢?

其实,在不加括号的时候,{} 被当成了一个独立的空代码块,所以 {} + [] 变成了 +[],结果就变成了 0

同样的问题还出现在 {} + {} 上,而且火狐和谷歌的结果还不一样:

> {} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"

如果 {} 被当成一个独立的代码块,那么这句话相当于 +{},相当于 Number({}),结果自然是 NaN,可是 Chrome 却在这里返回了正确的值。

那为什么这里就返回了正确的值呢?我也不知道,欢迎解答~

== 相等

规范

"==" 用于比较两个值是否相等,当要比较的两个值类型不一样的时候,就会发生类型的转换。

关于使用"=="进行比较的时候,具体步骤可以查看规范11.9.5

当执行x == y 时:

  1. 如果x与y是同一类型:

    1. x是Undefined,返回true
    2. x是Null,返回true
    3. x是数字:
      1. x是NaN,返回false
      2. y是NaN,返回false
      3. x与y相等,返回true
      4. x是+0,y是-0,返回true
      5. x是-0,y是+0,返回true
      6. 返回false
    4. x是字符串,完全相等返回true,否则返回false
    5. x是布尔值,x和y都是true或者false,返回true,否则返回false
    6. x和y指向同一个对象,返回true,否则返回false
  2. x是null并且y是undefined,返回true

  3. x是undefined并且y是null,返回true

  4. x是数字,y是字符串,判断x == ToNumber(y)

  5. x是字符串,y是数字,判断ToNumber(x) == y

  6. x是布尔值,判断ToNumber(x) == y

  7. y是布尔值,判断x ==ToNumber(y)

  8. x是字符串或者数字,y是对象,判断x == ToPrimitive(y)

  9. x是对象,y是字符串或者数字,判断ToPrimitive(x) == y

  10. 返回false

觉得看规范判断太复杂?我们来分几种情况来看:

1. null和undefined

console.log(null == undefined);

看规范第2、3步:

  1. x是null并且y是undefined,返回true
  1. x是undefined并且y是null,返回true

所以例子的结果自然为 true

这时候,我们可以回想在《JavaScript专题之类型判断(上)》中见过的一段 demo,就是编写判断对象的类型 type 函数时,如果输入值是 undefined,就返回字符串 undefined,如果是 null,就返回字符串 null

如果是你,你会怎么写呢?

下面是 jQuery 的写法:

function type(obj) {
    if (obj == null) {
        return obj + '';
    }
    ...
}

2. 字符串与数字

console.log('1' == 1);

结果肯定是true,问题在于是字符串转化成了数字和数字比较还是数字转换成了字符串和字符串比较呢?

看规范第4、5步:

4.x是数字,y是字符串,判断x == ToNumber(y)

5.x是字符串,y是数字,判断ToNumber(x) == y

结果很明显,都是转换成数字后再进行比较

3. 布尔值和其他类型

console.log(true == '2')

当要判断的一方出现 false 的时候,往往最容易出错,比如上面这个例子,凭直觉应该是 true,毕竟 Boolean('2') 的结果可是true,但这道题的结果却是false。

归根到底,还是要看规范,规范第6、7步:

6.x是布尔值,判断ToNumber(x) == y

7.y是布尔值,判断x ==ToNumber(y)

当一方出现布尔值的时候,就会对这一方的值进行ToNumber处理,也就是说true会被转化成1,

true == '2' 就相当于 1 == '2' 就相当于 1 == 2,结果自然是 false

所以当一方是布尔值的时候,会对布尔值进行转换,因为这种特性,所以尽量少使用 xx == truexx == false 的写法。

比如:

// 不建议
if (a == true) {}

// 建议
if (a) {}
// 更好
if (!!a) {}

4. 对象与非对象

console.log( 42 == ['42'])

看规范第8、9步:

  1. x不是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  1. x是对象,y不是字符串或者数字,判断ToPrimitive(x) == y

以这个例子为例,会使用 ToPrimitive 处理 ['42'],调用valueOf,返回对象本身,再调用 toString,返回 '42',所以

42 == ['42'] 相当于 42 == '42' 相当于42 == 42,结果为 true

到此为止,我们已经看完了第2、3、4、5、6、7、8、9步,其他的一概返回 false。

其他

再多举几个例子进行分析:

console.log(false == undefined)

false == undefined 相当于 0 == undefined 不符合上面的情形,执行最后一步 返回 false

console.log(false == [])

false == [] 相当于 0 == [] 相当于 0 == '' 相当于 0 == 0,结果返回 true

console.log([] == ![])

首先会执行 ![] 操作,转换成 false,相当于 [] == false 相当于 [] == 0 相当于 '' == 0 相当于 0 == 0,结果返回 true

最后再举一些会让人踩坑的例子:

console.log(false == "0")
console.log(false == 0)
console.log(false == "")

console.log("" == 0)
console.log("" == [])

console.log([] == 0)

console.log("" == [null])
console.log(0 == "\n")
console.log([] == 0)

以上均返回 true

其他

除了这两种情形之外,其实还有很多情形会发生隐式类型转换,比如if? :&&等情况,但相对来说,比较简单,就不再讲解。

深入系列

JavaScript 深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript 深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

commented

Null与数字,第2步,rprim=null应改成rprim=1

!!!正在整理类型转换,大大就更新了!

大大,==相等里面规范8、9翻译错了,应该是“是字符串或数字”

想问一下if([]){console.log("kkk")}里面又是什么转换过程呢?如果是要按false == [] 的话,后面的打印就不会执行,但是实际上后面的语句块的内容会执行

!! 哪位大哥给一个比较清晰的分析,是不是用到了一元操作符转换,我觉得!这个操作符考题还挺多的

commented

@mqyqingfeng 大大,console.log(0 == "\n")为什么是等于true?Number('\n')没搞明白为什么等于0,不是说如果有一个字符不是数字,结果都会返回 NaN么?为啥Number('\n')不是等于NaN

我比较模糊,怎么判断先用valueOf还是toString

commented

@zh-lc 先调 valueOf,如果 valueOf 方法返回的值转 Number 后是 NaN 的话再调用 toString

commented

@mqyqingfeng

console.log(null + 1);
按照规范的步骤进行分析:

  1. lprim = ToPrimitive(null) 因为null是基本类型,直接返回,所以 lprim = null
  2. rprim = ToPrimitive(1) 因为 1 是基本类型,直接返回,所以 rprim = null

第二条 写错了 rprim = 1

commented

@jjaimm
不知道我解释对不对
字符串中 \n 是换行
所以"\n" 相当于 " "
ToNumber(" ")转为 0
相当于cosole.log(0 == 0)

想问一下if([]){console.log("kkk")}里面又是什么转换过程呢?如果是要按false == [] 的话,后面的打印就不会执行,但是实际上后面的语句块的内容会执行

[] 转换成boolean是true 只有 6 种值可以被转换成 false,其他都会被转换成 true。
这6个值分别是undefined、null、0、空字符串、NaN、false

@mqyqingfeng 大大,console.log(0 == "\n")为什么是等于true?Number('\n')没搞明白为什么等于0,不是说如果有一个字符不是数字,结果都会返回 NaN么?为啥Number('\n')不是等于NaN

  • Number转换的时候会忽略所有前导的 0和前序的空格 应该是换行相当于所有都是空格
    你可以试下这个例子
  • console.log(Number(" 000123")) // 123

!! 哪位大哥给一个比较清晰的分析,是不是用到了一元操作符转换,我觉得!这个操作符考题还挺多的

!只是转化为Boolean然后取非。!!取非再取非,就相当于只是做了个类型转换

请问

console.log("" == [null])

为什么是true啊???

换句话说,为什么

ToPrimitive([null])

结果是"" ?????

@hanqizheng 抽象操作ToPrimitive会首先检查该值是否有valueOf()方法。如果有并且返回基本类型值,就使用该值进行强制类型转换,如果没有就使用toString()的返回值来进行强制类型转换。如果valueOf()和ToString()都不返回基本类型值,会产生TypeEror错误。[null].toString() 结果为 ""

@Zshuhao 谢谢

@mqyqingfeng 大大,console.log(0 == "\n")为什么是等于true?Number('\n')没搞明白为什么等于0,不是说如果有一个字符不是数字,结果都会返回 NaN么?为啥Number('\n')不是等于NaN

这个跟类型转换没啥关系。 是各个浏览器处理 if (false) {}的方式不一样。 chrome貌似是不论条件判断是否通过都会执行一遍里面的逻辑。 safari是不会执行里面的逻辑的。 貌似红宝书还是哪里说过这个来着具体忘记了

commented

对着规范看头头是道,可是我记不住啊

commented

chrome里:

[] + {}; // [object Object]
{} + []; // 0

@mqyqingfeng 大大,console.log(0 == "\n")为什么是等于true?Number('\n')没搞明白为什么等于0,不是说如果有一个字符不是数字,结果都会返回 NaN么?为啥Number('\n')不是等于NaN

我建议你直接看ES规范文档里列出来的StringNumericLiteral语法,String转Number本来就是一件很复杂的事情,不知道从哪个博客传出来的字母和数字混合就直接NaN这种说法,规范文档里重来没这么描述过,规范的明确表示是——“If the grammar cannot interpret the String as an expansion of StringNumericLiteral, then the result of ToNumber is NaN.”(如果String语法不符合StringNumericLiteral拓展的形式,就返回结果NaN);
从StringNumericLiteral的拓展语法来看,七种空格字符WhiteSpace,四种换行符LineTerminator出现在数字文本StrNumericLiteral的前后都是符合情况的,而空白字符单独出现也是语法情况之一,至于其他的形式还有二进制0b,八进制0o,十六进制0x;以及包含科学计数法的十进制小数形式(eE)等,例如1.23E8这种,也是符合语法规定的。

大大,==相等里面规范8、9翻译错了,应该是“是字符串或数字”

我也发现这个问题了

commented
  1. x不是字符串或者数字,y是对象,判断x == ToPrimitive(y)
  2. x是对象,y不是字符串或者数字,判断ToPrimitive(x) == y

大佬这里是不是打错了,我看了规范原文是这样的:

If Type(x) is either String or Number and Type(y) is Object,
return the result of the comparison x == ToPrimitive(y).

是不是应该写成:如果x是字符串或数字?

commented

http://yanhaijing.com/es5/#203
按照规范说法

最后两条是不是写错了,应该是

x是字符串或者数字且y是对象,判断x == ToPrimitive(y)
x是对象且y是字符串或者数字,判断ToPrimitive(x) == y


规范
若Type(x)为String或Number,且Type(y)为Object,返回比较x == ToPrimitive(y)的结果。
若Type(x)为Object且Type(y)为String或Number, 返回比较ToPrimitive(x) == y的结果。

{} + {}
// 火狐: NaN
// 谷歌: "[object Object][object Object]"
如果 {} 被当成一个独立的代码块,那么这句话相当于 +{},相当于 Number({}),结果自然是 NaN,可是 Chrome 却在这里返回了正确的值。

我觉得这里火狐的结果是对的,Chrome 返回 [object Object][object Object] 应该是他开发工具的问题,可能是相当于 console.log({} + {}) 了。
如果这样写 {} + {} == [object Object][object Object] ? console.log(1) : console.log(2),火狐和 Chrome 的结果是一样的

commented

请问==的规范里1.iii.f这条是不是少了个条件,直接就“返回false”了

想问一下if([]){console.log("kkk")}里面又是什么转换过程呢?如果是要按false == [] 的话,后面的打印就不会执行,但是实际上后面的语句块的内容会执行

应该是 if (condition) { } 中的 condition 会被隐式转换为 Boolean 类型,Boolean([]) 为 true。

console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"
这里Date经过valueOf执行后不就直接转成number了吗?为什么转成字符串了???

console.log(new Date(2017, 04, 21) + 1) // "Sun May 21 2017 00:00:00 GMT+0800 (CST)1"
这里Date经过valueOf执行后不就直接转成number了吗?为什么转成字符串了???

这里 Date 使用的是 toString,规范里有讲到

这个规则是不是可以总结成:
不同类型时;根据符号两边的数据类型按照优先顺序做隐式转换
优先级为,
1、对象
2、字符串
3、数字

对象会优先转化成数字或字符串(根据判断的另一个数据类型)再进行比较;
其中当有布尔对象时,转化为数字比较:
console.log(true == ['true'])//false
console.log(false == [])//true

commented

请问

console.log("" == [null])

为什么是true啊???

换句话说,为什么

ToPrimitive([null])

结果是"" ?????

@hanqizheng 建议参考:Array.prototype.join

想问一下if([]){console.log("kkk")}里面又是什么转换过程呢?如果是要按false == [] 的话,后面的打印就不会执行,但是实际上后面的语句块的内容会执行

if 的转换规则和 == 不一样,if(xxx) 你可以看成 Boolean(xxx)

关于 {} + {} 浏览器不同的输出,我刚好研究了下
原文在这
大概在chrome版本49之前,Chrome控制台上面的输出结果基本和Firefox一致,之后在chrome上有人提出bug,Issue 499864,大概意思就是说我在控制台输入{a: 4, b: 5}你给我报个错干嘛,我就是想要一个对象而已。Chrome没过多久就修复了,修复的方式也特别666,就是凡是语句以{开头,以}结尾,我解析的时候就包裹一层括号在外面。git记录,里面的关键代码如下:

+    if (/^\s*\{/.test(text) && /\}\s*$/.test(text))
+        text = '(' + text + ')';

故在chrome下{} + {} 即是({} + {})
故这里的花括号正确地被解析成了对象

想问一下if([]){console.log("kkk")}里面又是什么转换过程呢?如果是要按false == [] 的话,后面的打印就不会执行,但是实际上后面的语句块的内容会执行
if语句会直接调用Boolean