看不懂的 []+{} (隐式转换)

在平时的积累过程中有没有遇见过类似这样的代码:

1
2
3
[]+[]
{}+{}
1+[]

??这是什么,难道在代码中还真有人这样写?那他一定是疯了;好吧,虽然代码中不提倡写这种晦涩难懂还说不定就出错的代码,但是其原理是不是该找个文档查一查,看一看这究竟是怎么一回事呢?那么下面可以开始记录一下 JavaScript 代码世界中的隐式转换。

加法运算

在 JavaScript 中加法运算很简单,它只做 数字和字符串 的加法操作,所以不是这两种类型的都会被转换成这两种原始类型数据再进行操作。而在 JavaScript 中数据类型分为两种(原始数据类型和对象):

  • 原始数据类型(primitives): undefined, null, number, string, boolean, symbol(es6新增)
  • 对象: 包括数组、函数

那么对象是怎么转换成原始数据类型的呢?

ToPrimitive 方法

在 JavaScript 内部有一个 ToPrimitive() 方法,它用于将对象转换成原始数据类型。

1
ToPrimitive(input, PreferredType?);

这个函数接收两个参数:

  • input: 输入值
  • PreferredType: 此参数可以试 Number 或者 String,它代表我们的对象会优先转换成哪种原始数据类型。 如果缺少这个参数,对于 Date 的实例,默认为 String,其余的都为 Number; 转换步骤也会因为这个参数的不同而不同。
    下面看一下它的转换过程。

PreferredType 为 Number

  1. input 为原始数据类型,直接返回 input;
  2. 如果 input 是一个对象,那么它会先去调对象的 valueOf() 方法,得到的结果若为原始数据类型则返回;
  3. 如果上一步得到的结果仍然是对象,那么此时会去调用对象的 toString() 方法,得到的结果为原始数据类型则返回;
  4. 如果上一步操作完之后得到的仍不是原始数据类型,那么此时就抛出错误。

PreferredType 为 String

  1. input 为原始数据类型,直接返回 input;
  2. 如果 input 是一个对象,那么它会先去调对象的 toString() 方法,得到的结果若为原始数据类型则返回;
  3. 如果上一步得到的结果仍然是对象,那么此时会去调用对象的 valueOf() 方法,得到的结果为原始数据类型则返回;
  4. 如果上一步操作完之后得到的仍不是原始数据类型,那么此时就抛出错误。
    这里de PreferredType 为 Number 和 String 两个不同值处理转换的步骤略有不同,主要区别就是根据数据类型判断先执行 valueOf() 方法还是 toString() 方法,交换上面执行步骤的2、3步。
    那么 valueOf() 和 toString() 究竟返回的是什么?

valueOf() 和 toString()

valueOf() 和 toString() 都是 Object 的属性,那它们返回的都是什么?例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const data1 = {
a: 1
};
console.log(data1.valueOf()); // {a:1}
console.log(data1.toString()); // "[object Object]"

const data2 = [1,2,3];
console.log(data2.valueOf()); // [1,2,3]
console.log(data2.toString()); // "1,2,3"

const data3 = function() {
const a = 1;
return a;
}
console.log(data3.valueOf()); // function() {const a = 1; return a;}
console.log(data3.toString()); // "function() {const a = 1; return a;}"

这里的几个console打印数据能搞清楚?不要懵逼,直接控制台一看不就记住了;

  • 对象: valueOf() 返回对象本身,toString() 返回字符串 [object Object];
  • 数组: valueOf() 返回数组本身,toString() 返回数组值用逗号链接的字符串,和 join(‘,’) 得到的结果一样;
  • 方法: valueOf() 返回方法本身,toString() 返回的是Function中改写的toString()处理完的字符串结果;

+ 运算

最后回归正题,加号(+)的运算,就是对当前进行加法运算的值进行转换后再做相应的操作,如:

1
value1 + value2

在上面的加法运算表达式中,会是如下操作步骤:

  1. 将两个操作数转换成基本数据类型

    1
    2
    3
    prim1 = ToPrimitive(value1);
    prim2 = ToPrimitive(value2);
    // 这里省略了 PreferredType 参数,因此日期值为 String,其他的为 Number
  2. 如果 prim1 和 prim2 是一个字符串,则将其转成字符串并返回相连接的结果

  3. 否则,将 prim1 和 prim2 都转为数字并返回结果的综合

那么最开始的代码

1
2
3
[]+[]
{}+{}
1+[]

进行运算后得到的结果是什么呢?

  1. []+[] 首先将[]进行原始数据转换,也就是 ToPrimitive([]),因为是数组,所以 PreferredType 为 Number;经过转换步骤2([].valueOf())后仍是一个对象,那么再进行步骤3([].toString()),得到的结果就是一个空字符串,两个空字符串相加还是一个空字符串(””));
  2. {}+{} 首先将{}进行原始数据转换,也就是 ToPrimitive({}),因为是对象,所以 PreferredType 为 Number;经过转换步骤2([].toString())后仍是一个字符串”[object Object]”,已经是原始数据类型,不需要在进行转换,那么得到的结果就是两个字符串相加的结果(”[object Object][object Object]”);
  3. 1+[] 首先对1和[]进行原始数据转换,也就是 ToPrimitive(1) 和 ToPrimitive([]),因为1已经是原始数据类型,则直接返回值1;而[]是数组,则 PreferredType 为 Number,经过步骤2([].valueOf())后仍是一个对象,再进行步骤3([].toString()),得到了一个空字符串;最后,就是 1+”” ,得到的结果是字符串 “1”;

而我在进行测试的时候发现了一个有意思的事情是:{}+[]{}+1 得到的结果并不是 “[object Object]” 和 “[object Object]1”,讲道理,根据我们前面的操作,这里应该就是这个结果,那这是为什么呢?最后发现这是不同的浏览器的解析不同,它会将前面的 {} 当成一个代码块,于是得到的结果就会变成 +""+1,即为 ""1

最后

在这种有隐式转换的操作中,搞清楚其操作步骤就ok了,但这里要注意的是 {} 在前面时有可能得到的值并不是你想要的。