You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

11 KiB

07类型转换V8是怎么实现1+“2”的

你好,我是李兵。

前面我们花了很多篇幅聊了JavaScript中最基础却很容易被忽略的“对象”以及V8是怎么处理“对象”的今天我们继续来聊另一个非常基础同时也是很容易被大家忽略的问题那就是JavaScript中的“类型系统”。

在理解这个概念之前你可以先思考一个简单的表达式那就是在JavaScript中“1+2等于多少

其实这相当于是在问在JavaScript中让数字和字符串相加是会报错还是可以正确执行。如果能正确执行那么结果是等于数字3还是字符串“3”还是字符串“12”呢

如果你尝试在Python中使用数字和字符串进行相加操作那么Python虚拟机会直接返回一个执行错误错误提示是这样的

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

这段错误代码提示了这是个类型错误表明Python并不支持数字类型和字符串类型相加的操作。

不过在JavaScript中执行这段表达式是可以返回一个结果的最终返回的结果是字符串“12”。

最终结果如下所示:

>>> 1+'2'
>>> "12"

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

什么是类型系统(Type System)

在这个简单的表达式中涉及到了两种不同类型的数据的相加。要想理清以上两个问题我们就需要知道类型的概念以及JavaScript操作类型的策略。

对机器语言来说所有的数据都是一堆二进制代码CPU处理这些数据的时候并没有类型的概念CPU所做的仅仅是移动数据比如对其进行移位相加或相乘。

而在高级语言中,我们都会为操作的数据赋予指定的类型,类型可以确认一个值或者一组值具有特定的意义和目的。所以,类型是高级语言中的概念。

比如在C/C++中,你需要为要处理的每条数据指定类型,这样定义变量:

int counter = 100 # 赋值整型变量
float miles = 1000.0 # 浮点型
char* name = "John" # 字符串

C/C++编译器负责将这些数据片段转换为供CPU处理的正确命令通常是二进制的机器代码。

在某些更高级的语言中还可以根据数据推断出类型比如在Python或JavaScript中你就不必为数据指定专门的数据类型在Python中你可以这样定义变量

counter = 100 # 赋值整型变量
miles = 1000.0 # 浮点型
name = "John" # 字符串

在JavaScript中你可以这样定义变量

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

虽然Python和JavaScript定义变量的方式不同但是它们都不需要直接指定变量的类型因为虚拟机会根据数据自动推导出类型。

通用的类型有数字类型、字符串、Boolean类型等等引入了这些类型之后编译器或者解释器就可以根据类型来限制一些有害的或者没有意义的操作。

比如在Python语言中如果使用字符串和数字相加就会报错因为Python觉得这是没有意义的。而在JavaScript中字符串和数字相加是有意义的可以使用字符串和数字进行相加的。再比如你让一个字符串和一个字符串相乘这个操作是没有意义的所有语言几乎都会禁止该操作。

每种语言都定义了自己的类型,还定义了如何操作这些类型,另外还定义了这些类型应该如何相互作用,我们就把这称为类型系统

关于类型系统,wiki百科上是这样解释的:

在计算机科学中类型系统type system用于定义如何将编程语言中的数值和表达式归类为许多不同的类型如何操作这些类型这些类型如何互相作用。

直观地理解,一门语言的类型系统定义了各种类型之间应该如何相互操作,比如,两种不同类型相加应该如何处理,两种相同的类型相加又应该如何处理等。还规定了各种不同类型应该如何相互转换,比如字符串类型如何转换为数字类型。

一个语言的类型系统越强大,那编译器能帮程序员检查的东西就越多,程序员定义“检查规则”的方式就越灵活。

V8是怎么执行加法操作的

了解了JavaScript中的类型系统接下来我们就可以来看看V8是怎么处理1+“2”的了。

当有两个值相加的时候,比如:

a+b

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

具体细节你也可以参考规范,我将标准定义的内容翻译如下:

AdditiveExpression : AdditiveExpression + MultiplicativeExpression

  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的错误。

将对象转换为原生类型的流程图如下所示:

当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中的类型系统是怎么工作的。类型系统定义了语言应当如何操作类型以及这些类型如何互相作用。因为Python和JavaScript的类型系统差异所以当处理同样的表达式时返回的结果是不同的。

在Python中数字和字符串相加会抛出异常这是因为Python认为字符串和数字相加是无意义的所以限制其操作。

在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"

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

感谢你的阅读,如果你觉得这一讲的内容对你有所启发,也欢迎把它分享给你的朋友。