【js小知识】[]+ {} =?/{} +[] =?(关于加号的隐式类型转换)

3,132

写在前面

今天来聊一聊JS中的加号,就是这个小小的‘+’,你可能会以为加法不久两种情况,数字和字符串啊。我最开始也是这么认为的,后来真香…(小加号解决了我最近项目的一个小问题放在文末来说)

往往越是细小的知识点越容易被忽视。越细小的知识越能考验基础比。如{}+{}的结果是什么?[]+{}的结果是什么?

如果你还知道那就花十分钟康康这篇文章吧。

js的数据类型

在js中有两类数据类型:

  • 原始类型:String、Number、Null、Boolean、Undefined、Symbol、Bigint
  • 引用类型:Object(包括数组array和函数function)

特别提醒:MDN文档中js的数据类型有8中,7中数据类型和对象类型,Bigint可以用任意精度表示整数,这篇文章不会使用

八种数据类型传送门:MDN文档

加法的一元运算

加法一元运算的内部操作

  • 语法格式: + Expression
  • 说明: 加法的一元运算中,Expression会进行Number(Expression)操作。
    Number是一个将参数转换成数字的方法。

不同类型参数返回的结果

实践检验

在控制台中输入以下一元运算符,可以看到他们的结果

结果分析

  • +ace运算分析:

由于ace是一个没有定义的变量,那么应该是undefined类型,所以Tonumber返回的结果是NaN。

  • +''运算分析 :

这里加号后面是一个空字符串,空字符串转数字就是0,结果返回0。

  • +false的运算分析:

false是一个布尔值,转换成数字就是0。

  • +'123'和+'qqq'的运算分析:

这两个加号后都是字符串,前者是纯数字,后者是字符,所以前面能够转换成数字,后面的非树转换成NaN。

  • +{}和+[]的分析:

这里加号后面是对象类型,对象类型会先转换成原始值(ToPrimitice方法),然后将这个原始值再转换成数字类型。关于ToPrimitice方法将在后面详细介绍。

ToPrimitive(input,PreferredType?)方法介绍

ToPrimitice是JavaScript引擎内部的一个操作,将参数input转换成原始值。内部执行原理有四部

  1. 如果这个参数本身就是原始值,那么返回本身。

  2. 如果不是参数调用ValueOf方法,valueOf方法返回的结果如果是原始值就返回该原始值。

  3. 如果不是就调用toString方法,toString方法的结果如果是原始值就返回该值。

  4. 如果结果还不是原始值,抛出 TypeError 异常。

tips1

如果valueOf和toString方法不了解,参考NMD官方文档,这里给出链接。valueOf方法官方文档,另外给出toString方法实践的截图

tips2

参数 PreferredType 可以是 Number 或者 String。如果ToPrimitive(input,PreferredType?)中PreferredType 被标志为 String,则转换操作的第二步和第三步的顺序会调换。 如果没有 PreferredType 这个参数,则 PreferredType 的值会按照Number类型上面的四步处理。


是否还记得前面一元运算的实践小案例呢?

+{}会进过ToPrimitive方法转换成原始值后再进行toNumber。最后的结果是NaN,一起来看看这个转换的过程吧

//先ToPrimitive({})再Number()
+{}
Number({})
Number({}.valueOf());//结果是{}非原始值 {}.valueOf() isn’t primitive
Number({}.toString())  //结果是字符串,为原始值
Number("[object Object]")//"[object Object]"非数字的字符串
NaN

同理,+[]也是一样大的道理

Number([])
Number([].toString())//[].valueOf() isn’t primitive
Number('')
0

加法的二元运算

内部原理

有下面这样的一个加法操作。

value1 + value2

在计算这个表达式时,内部的操作步骤是这样的

将两个操作数转换为原始值 (以下是数学表示法的伪代码,不是可以运行的 JavaScript 代码):
prim1 := ToPrimitive(value1) prim2 := ToPrimitive(value2)
PreferredType 被省略,因此 Date 类型的值采用 String,其他类型的值采用 Number。

如果 prim1 或者 prim2 中的任意一个为字符串,则将另外一个也转换成字符串,然后返回两个字符串连接操作后的结果。

否则,将 prim1 和 prim2 都转换为数字类型,返回他们的和。

实践检验

我们来看看文章开头提到的例子,{}+{}、[]+[]这些骚操作。

{}+{}的结果

对于{}+{},加号的两边都是对象类型。先使用ToPrimitive将他们转换成字符串类型。回顾一下ToPrimitive方法内部转换四个过程,应该调用toString将他们转换为两个相同的字符串。相加的结果还是一个字符串。

我们来四步分析看看吧

//ToPrimitive({})
1. {}是对象类型非原始值
2. {}.valueOf()f方法返回的结果还是非原始值
3. {}.toString()方法返回结果是"[object Object]"字符串
4. 是一个原始值

所以上面的{}+{}的结果是两个"[object Object]"字符串相加,拼接为的字符串"[Object Object][object Object]"就是最后的结果。

{}+[]的结果

这个{}+[]和上满一样的道理,只不过[].toString返回的结果是是一个空字符,这里为了方便大家,截图展示一下。


但是对于toString和valueOf()方法一定要熟悉,不知道就去查。

所以{}+[]的结果就是"[object Object]"字符串。

可能有的人会对上面的分析表示怀疑,我在终端中输入这两个二元运算,打印的结果和我们分析的一样,附上结果图:

一个有趣的现象

细心的朋友看到上面的截图可能会有一个疑问。
一样吗?不一样啊,{}+[]的结果是0啊,我们上面的分析不应该是一个字符串吗?这是为什么,别急,下面将为你慢慢道来。

原因:{}+[]是把{}解析为为一个空的代码块并且忽略了它,可能有人问什么是代码块。比如你写的一个箭头函数()=>{},这里面的大括号{}内的内容就是代码块。

在{}+[]中忽略了空的代码块,就变成了了一元运算+[]。所以结果是0。当我们让数组放在前面,对象放在后面的时候[]+{}就是我们前面分析的结果。总之不能让{}出现在开头。

拓展

这里再拓展一下,{}.toString()方法报错


这和前面是一个道理,大括号开头的js引擎会认为是代码块,所以会报错。改成({}).toString()就好了。

文章末尾

参考文章

文章的灵感

这篇文章的灵感来源最近在做一个小项目,项目里有一个数据分页功能。分页功能中上一页就是当前页减去1,那么下一页就是当前页加1咯。但是最后报错了。

报错的原因:当前的页码是从url地址栏中获取的,获取的当前页page是字符串类型。字符串的减法存在隐式类型转换(转换成数字);字符串加上一个数字的结果还是字符串不是数字

解决错误:转换思路,让当前页-0隐式转换成数字后再加1就可以啦。

通过这个小事儿我就是想说一些很细小的东西往往是大家容易忽略的,每个知识也都是有他的意义的。

看完的收获

希望看完这篇文章的你收获了以下知识:

  • valueOf()和toString()方法的运用
  • ToPrimitive(input,PreferredType?)方法的原理
  • 任意数据类型的一元加法运算
  • 任意数据类型的二元加法运算

其中最核心最重要的就是ToPrimitive(input,PreferredType?)方法的原理,再次回顾一下这个原理:这个方法是将某个类型的数据转换成原始类型,他的返回值一定是原始值,如果参数input本身就是原始值就返回这个原始值;

如果不是就调用valueOf方法或者toString方法,至于先调用哪一个看PreferredType是number还是string,默认情况下是number,即先进行valueOf方法看结果是否是原始值,如果不是再使用tosting方法看返回的结果是否是原始值;如果还不是就会报错。

最后,走心文章,希望能够用心看完,如有错误地方欢迎评论区指正


下篇文章见…