类型转换:V8是怎么实现1+“2”的?
JavaScript中,“1+‘2’等于多少?” '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'
什么是类型系统(Type System)?
在这个简单的表达式中,涉及到了两种不同类型的数据的相加。要想理清以上两个问题,我们就需要知道类型的概念,以及JavaScript操作类型的策略。
对机器语言来说,所有的数据都是一堆二进制代码。
而在高级语言中,我们都会为操作的数据赋予指定的类型,类型可以确认一个值或者一组值具有特定的意义和目的。所以,类型是高级语言中的概念。
比如在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定义方式不同,但是他们都不需要直接指定变量的类型,因为虚拟机会根据数据自动推导出类型。
每种语言都定义了自己的类型,还定义了如何操作这些类型,另外还定义了这些类型应该如何相互作用,我们就把这称为类型系统。关于类型系统,
wiki百科上是这样解释的:
在计算机科学中,类型系统(type system)用于定义如何将编程语言中的数值和表达式归类为许多不同的类型,如何操作这些类型,这些类型如何互相作用。
V8是怎么执行加法操作的?
了解了JavaScript中的类型系统,接下来我们就可以来看看V8是怎么处理1+“2”的了。
V8会严格根据ECMAScript规范来执行操作。ECMAScript是一个语言标准,JavaScript就是ECMAScript的一个实现,比如在ECMAScript就定义了怎么执行加法操作,如下所示:
具体细节你也可以参考规范,我将标准定义的内容翻译如下:
AdditiveExpression : AdditiveExpression + MultiplicativeExpression
- 把第一个表达式(AdditiveExpression)的值赋值给左引用(lref)。
- 使用GetValue(lref)获取左引用(lref)的计算结果,并赋值给左值。
- 使用ReturnIfAbrupt(lval)如果报错就返回错误。
- 把第二个表达式(MultiplicativeExpression)的值赋值给右引用(rref)。
- 使用GetValue(rref)获取右引用(rref)的计算结果,并赋值给rval。
- 使用ReturnIfAbrupt(rval)如果报错就返回错误。7.使用ToPrimitive(lval)获取左值(lval)的计算结果,并将其赋值给左原生值(lprim)。
- 使用ToPrimitive(rval)获取右值(rval)的计算结果,并将其赋值给右原生值(rprim)。
- 如果Type(lprim)和Type(rprim)中有一个是String,则:
a. 把ToString(lprim)的结果赋给左字符串(lstr);
b. 把ToString(rprim)的结果赋给右字符串(rstr);
c. 返回左字符串(lstr)和右字符串(rstr)拼接的字符串。 - 把ToNumber(lprim)的结果赋给左数字(lnum)。
- 把ToNumber(rprim)的结果赋给右数字(rnum)。
- 返回左数字(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 // 103
上面我们介绍过了,由于需要先使用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
// VM263:9 Uncaught TypeError: Cannot convert object to primitive value
// at <anonymous>:9:6
因为ToPrimitive会先调用valueOf方法,发现返回的是一个对象,并不是原生类型,当ToPrimitive继续调用toString方法时,发现toString返回的也是一个对象,都是对象,就无法执行相加运算了,这时候虚拟机就会抛出一个异常。
所以说,在执行加法操作的时候,V8会通过ToPrimitive方法将对象类型转换为原生类型,最后就是两个原生类型相加,如果其中一个值的类型是字符串时,则另一个值也需要强制转换为字符串,然后做字符串的连接运算。在其他情况时,所有的值都会转换为数字类型值,然后做数字的相加。
此文章为5月Day14学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。