了解V8(二)类型转换:V8是怎么实现1+“2”的?

各位小伙伴们好,今天咱们来聊一聊JavaScript 中的“类型系统”。函数

可是在开始以前呢咱们能够先思考一个简单的表达式,那就是在 JavaScript 中,“1+‘2’等于多少?”spa

其实这至关因而在问,在 JavaScript 中,让数字字符串相加是会报错,仍是能够正确执行。翻译

若是能正确执行,那么结果是等于数字 3,仍是字符串“3”,仍是字符串“12”呢?code

若是你尝试用一些其余语言执行数字了字符串相加,会是什么杨的结果呢。对象

好比说用 Python 使用数字和字符串进行相加操做,则会直接返回一个执行错误,错误提示是这样的:ip

>>>1+'2'
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
  TypeError: unsupported operand type(s) for +: 'int' and 
  'str'

可是在 JavaScript 中执行这段表达式,倒是能够返回一个结果的,最终返回的结果是字符串“12”。rem

那么为何一样的表达式,在 Python 和 JavaScript 中执行为何会有不一样的结果?为何在 JavaScript 中执行,输出的是字符串“12”,不是数字 3 或者字符串“3”呢?

什么是类型系统 (Type System)?

在上边的表达式中,涉及到了两种不一样类型的数据的相加。要想理清以上两个问题,咱们就须要知道类型的概念,以及 JavaScript 操做类型的策略。字符串

对机器语言来讲,全部的数据都是一堆二进制代码,CPU 处理这些数据的时候,并无类型的概念,CPU 所作的仅仅是移动数据,好比对其进行移位,相加或相乘。虚拟机

而在高级语言中,咱们都会为操做的数据赋予指定的类型,类型能够确认一个值或者一组值具备特定的意义和目的。因此,类型是高级语言中的概念it

image.png

在 JavaScript 中,你能够这样定义变量:

var num = 100 # 赋值整型变量
  let miles = 1000.0 # 浮点型
  const name = "John" # 字符串

V8 是怎么执行加法操做的?

了解了类型系统,接下来咱们就能够来看看 V8 是怎么处理 1+“2”的了。当有两个值相加的时候,好比:

a+b

V8 会严格根据 ECMAScript 规范来执行操做。ECMAScript 是一个语言标准,JavaScript 就是 ECMAScript 的一个实现,好比在 ECMAScript 就定义了怎么执行加法操做,以下所示:

image.png
具体细节你也能够参考规范,我将标准定义的内容翻译以下:

  1. 把第一个表达式 (AdditiveExpression) 的值赋值给左引用 (lref)。
  2. 使用 GetValue(lref) 获取左引用 (lref) 的计算结果,并赋值给左值。
  3. 使用ReturnIfAbrupt(lval) 若是报错就返回错误。
  4. 把第二个表达式 (MultiplicativeExpression) 的值赋值给右引用 (rref)。
  5. 使用 GetValue(rref) 获取右引用 (rref) 的计算结果,并赋值给 rval。
  6. 使用ReturnIfAbrupt(rval) 若是报错就返回错误。
  7. 使用 ToPrimitive(lval) 获取左值 (lval) 的计算结果,并将其赋值给左原生值 (lprim)。
  8. 使用 ToPrimitive(rval) 获取右值 (rval) 的计算结果,并将其赋值给右原生值 (rprim)。
  9. 若是 Type(lprim) 和 Type(rprim) 中有一个是 String,则:

    a. 把 ToString(lprim) 的结果赋给左字符串 (lstr);

    b. 把 ToString(rprim) 的结果赋给右字符串 (rstr);

    c. 返回左字符串 (lstr) 和右字符串 (rstr) 拼接的字符串。

  10. 把 ToNumber(lprim) 的结果赋给左数字 (lnum)。
  11. 把 ToNumber(rprim) 的结果赋给右数字 (rnum)。
  12. 返回左数字 (lnum) 和右数字 (rnum) 相加的数值。

通俗地理解,V8 会提供了一个 ToPrimitive 方法,其做用是将 a 和 b 转换为原生数据类型,其转换流程以下:

  • 先检测该对象中是否存在 valueOf 方法,若是有并返回了原始类型,那么就使用该值进行强制类型转换;
  • 若是 valueOf 没有返回原始类型,那么就使用 toString 方法的返回值;
  • 若是 vauleOf 和 toString 两个方法都不返回基本类型值,便会触发一个 TypeError 的错误。

image.png

当 V8 执行 1+“2”时,由于这是两个原始值相加,原始值相加的时候,若是其中一项是字符串,那么 V8 会默认将另一个值也转换为字符串,至关于执行了下面的操做:Number(1).toString() + "2"

这里,把数字 1 偷偷转换为字符串“1”的过程也称为强制类型转换,由于这种转换是隐式的,因此若是咱们不熟悉语义,那么就很容易判断错误。

咱们还能够再看一个例子来验证上面流程,你能够看下面的代码:

var Obj = {
      toString() {
        return '200'
      }, 
      valueOf() {
        return 100
      }   
    }
    Obj+3

执行这段代码,你以为应该返回什么内容呢?

上面咱们介绍过了,因为须要先使用 ToPrimitive 方法将 Obj 转换为原生类型,而 ToPrimitive 会优先调用对象中的 valueOf 方法,因为 valueOf 返回了 100,那么 Obj 就会被转换为数字 100,那么数字 100 加数字 3,那么结果固然是 103 了。

若是我改造下代码,让 valueOf 方法和 toString 方法都返回对象,其改造后的代码以下:

var Obj = {
      toString() {
        return new Object()
      }, 
      valueOf() {
        return new Object()
      }   
  }
  Obj+3

再执行这段代码,你以为应该返回什么内容呢?

由于 ToPrimitive 会先调用 valueOf 方法,发现返回的是一个对象,并非原生类型,当 ToPrimitive 继续调用 toString 方法时,发现 toString 返回的也是一个对象,都是对象,就没法执行相加运算了,这时候虚拟机就会抛出一个异常,异常以下所示:

VM263:9 Uncaught TypeError: Cannot convert object to primitive value
    at <anonymous>:9:6

提示的是类型错误,错误缘由是没法将对象类型转换为原生类型。

因此说,在执行加法操做的时候,V8 会经过 ToPrimitive 方法将对象类型转换为原生类型,最后就是两个原生类型相加,若是其中一个值的类型是字符串时,则另外一个值也须要强制转换为字符串,而后作字符串的链接运算。在其余状况时,全部的值都会转换为数字类型值,而后作数字的相加。

总结

今天咱们主要了解了 JavaScript 中的类型系统是怎么工做的。类型系统定义了语言应当如何操做类型,以及这些类型如何互相做用。

在 JavaScript 中,数字和字符串相加会返回一个新的字符串,这是由于 JavaScript 认为字符串和数字相加是有意义的,V8 会将其中的数字转换为字符,而后执行两个字符串的相加操做,最终获得的是一个新的字符串。

在 JavaScript 中,类型系统是依据 ECMAScript 标准来实现的,因此 V8 会严格根据 ECMAScript 标准来执行。

在执行加法过程当中,V8 会先经过 ToPrimitive 函数,将对象转换为原生的字符串或者是数字类型,在转换过程当中,ToPrimitive 会先调用对象的 valueOf 方法,若是没有 valueOf 方法,则调用 toString 方法,若是 vauleOf 和 toString 两个方法都不返回基本类型值,便会触发一个 TypeError 的错误。

思考题

咱们一块儿来分析一段代码:

var Obj = {
    toString() {
      return "200"
    }, 
    valueOf() {
      return 100
    }   
  }
  Obj+"3"

你以为执行这段代码会打印出什么内容呢?欢迎你在留言区与我分享讨论。

相关文章
相关标签/搜索