JavaScript 中神奇的加法

版权声明:转载请声明原地址 https://blog.csdn.net/dk2290/article/details/86534595

一般来说, 加减法应该是我们学习生涯中接触到的第一个运算符,通常意义下它也是最简单的运算符。

在程序语言中,加减法的情况一般也比较简单,但是在 JavaScript 中加法的情况却比较奇怪,因为它有着大量特殊的情况。

我们举个简单的例子:

1 + '1' = '11'; 
1 + 'a' = '1a';
1 + []  = '';

从基础数据类型的加法开始,我们得到的结果就变的奇怪了起来。究其根由,其实是 JavaScript 的隐式转换在做怪。

一.什么是 JavaScript 隐式转换?

在讲隐式转换之前,咱们先得回顾一下 JavaScript5 种基础数据类型,3种引用数据类型和 3 个特殊值:

基础数据类型:

  1. number 类型
  2. string 类型
  3. boolean 类型
  4. null 类型
  5. undefined 类型

引用数据类型:

  1. object
  2. function
  3. array

特殊值:

  1. NaN
  2. +Infinity 和 -Infinity
  3. +0 和 -0

ok,我们回到隐式转换中,正如我开始举的那个例子一样,JavaScript 的隐式转换总是发生在各种运算符以及特殊的真值判断中(比如说 if),对与 java 来说,一个 Number 类型值 + 一个 Boolean 类型值 是肯定会报错的,但是在 JavaScript 中却不会,因为隐式转换会将 Boolean 类型的值转换为 Number 值。

二. JavaScript 的加法中隐式转换是怎么工作的?

你在百度上搜索 JavaScript 加法特性,你也许会看到这种图解:
在这里插入图片描述
或者是这种:
在这里插入图片描述

他们是很有用处的内容总结,但是光靠它们并不能帮我们更好的去理解,所以我们需要实例来支撑理论。

我们先来看看一个有趣的例子:

// number + ?
1 + 1 = 2;  // number
1 + '1' = "11";  // string
1 + true = 2; // number
1 + null =  1; //number
1 + undefined = NaN  // NaN
1 + {} = "1[object Object]"  // string
1 + [] = "1";  // string 


// string + ?
'1' + 1 = "11";  // string
'1' + '1' = "11";   // string
'1' + true = "1true";   // string
'1' + null = "1null";   // string
'1' + undefined = "1undefined";   // string
'1' + {} = "1[object object]";   // string
'1' + [] = "1";  // string

看到这大家是不是有点晕?我之所以举了这两个例子,是因为在 JavaScript 的加法中其实只有两种规则:一种是 number + number,另一种是 string + string

我们从 string + ?的例子中很容易的可以得出一个结论,只要在某一个加法中,某一方是字符串,那么最后的结果一定是字符串。

但是这个结论只能解决我们很小的一部分疑惑,你可能存在类似下面的问题:

number + boolean = ?
number + null = ?
number + undefined = ?
number + object = ?

我觉得,想要理解这其中的转换,先得理解的是在一个加法运算中,隐式转换的潜规则到底是什么。

首先,在 JavaScript 的加法中,会发生三种转换:

1. 原始类型转换 ToPrimitive
2. 数字类型转换 ToNumber
3. 字符串类型转换 ToString

而转换的顺序永远是 ToPrimitive也就是原始类型转换优先。

那么什么是 原始类型转换

我们可以把原始类型转换分为两种,第一种是简单基本类型之间互转,比如:

number -> string
string -> number
null -> number
...

这其中的转换没什么要注意的地方,大家把我在上面借用过来的图里的内容记下来就行。

关键点在于第二种,复杂基本类型到简单基本类型的转换:

object -> number
object -> string

array -> number
array -> string

function -> number
function -> string

okok,有了上面的基础,我们再来讲讲原始类型转换的工作原理,老规矩,我们打开ECMAScript的官网ecmascript 5.1规范,找到 9.1 节,里面就有关于 ~ToPrimitive

我给大家放下原版内容:
在这里插入图片描述

大概解释如下
ToPrimitive 接受一个值 input,和一个可选的期望类型 PreferredType 作参数。
可选参数 PreferredType 可以是 Number 或者 String

如果对象有能力被转换为不止一种原语类型,可以使用可选的 PreferredType 类型 来暗示那个类型,但转换结果一定是一个原始值。

如果 PreferredType 被标志为 Number,则会进行下面的操作来转换input:

  1. 如果 input 是个原始值,则直接返回它。 否则,如果 input 是一个对象。则调用 obj.valueOf() 方法。
  2. 如果返回值是一个原始值,则返回这个原始值。 否则,调用 obj.toString() 方法。 如果返回值是一个原始值,则返回这个原始值。
  3. 否则,抛出 TypeError异常。如果PreferredType被标志为String,则转换操作的第二步和第三步的顺序会调换。

如果没有 PreferredType 这个参数,则PreferredType的值会按照这样的规则来自动设置:

  1. Date 类型的对象会被设置为 String
  2. 其它类型的值会被设置为 Number

OK,现在我们知道了 原始数据类型转换的大致工作流程,我们来看看在一个 加法 中会发生什么?

有两个参数 value1value2,有以下算式:

value1 + value2

这时在 JavaScript 引擎内部会发生以下这三步:

  1. 通过 ToPrimitivevalue1value2 转化为原始值,此时 PreferredType参数是被忽略的,所以除了 Date 类型外其他类型都会按照 Number 参数来处理。
  2. 此时如果 value1value2 中有一个 string 类型,调用 ToString 方法将另一个参数转化为 string 类型并进行字符串的拼接。
  3. 如果 value1value2 中一个 string 类型都没有,那么则通过 ToNumber 方法将两个参数都转化为 number 类型。

我举几个例子帮助大家理解下:

1 + '2' = '12' ;

1被 ToPrimitive 返回其自身的原始值 number, '2’的原始值为 string ,命中条件2,所以1被 ToString 转化为 ‘1’, ‘1’ 与 ‘2’ 进行拼接后得到 ‘12’;

再看一个例子:

1 + [] = '1';

1被 ToPrimitive 返回其自身的原始值 number, [] 则不太一样,首先它会调用 valueOf()函数[].valueOf() === []得到 array,这并不是原始值,所以它会进行 toString() 转换,得到空字符串 ""。然后""与 1 相加显然就是拼接了,最终得到'1';

再来一个:

1 + {} = 1;

1被 ToPrimitive 返回其自身的原始值 number,{} 与 [] 又不太一样,{} 为对象,它会进行ToPrimitive({}, string) 操作,此时它与 Number类型中的步骤2,3是相反的,也就是说它会直接调用 toString() 方法,得到 "[object object]",类型为 string,此时又命中条件2,1被转化为'1',进行字符串拼接后得到"1[object object]"

现在是不是清晰多了?整个工作原理,我们可以把它分为三部分:
1.简单基本类型之间的转化规则:

2.原始基本类型的转化原则。
3.加法中的执行顺序。

三. 害人的的语句优先

有这么一种情况,会让人疑惑:
在浏览器控制台输入以下代码:

{} + 1   //  return 1 number;
1 + {}   //return '1[object object]' string

如图:
在这里插入图片描述

还有更让人疑惑的:

{} + 1  //  return 1 number;
console.log({} + 1)  //  logs '1[object object]' string

如图:
在这里插入图片描述

这一切的根源就在于 {}的特殊表现,实际上在 JavaScript 的执行上下文中,{}有以下三种含义:

  1. 语句块
  2. 函数
  3. 对象字面量

而其中 语句块 这个含义的优先级是最高的

所以,以下这段代码中的{}相当于全局环境中的 label 语句了:

{} + 1 

实际上等于:

{};+1 

即:

+1 

猜你喜欢

转载自blog.csdn.net/dk2290/article/details/86534595