各位小伙伴们好,今天我们来聊一聊JavaScript 中的“类型系统”。
但是在开始之前呢我们可以先思考一个简单的表达式,那就是在 JavaScript 中,“1+‘2’等于多少?”
其实这相当于是在问,在 JavaScript 中,让数字和字符串相加是会报错,还是可以正确执行。
如果能正确执行,那么结果是等于数字 3,还是字符串“3”,还是字符串“12”呢?
如果你尝试用一些其他语言执行数字了字符串相加,会是什么杨的结果呢。
比如说用 Python 使用数字和字符串进行相加操作,则会直接返回一个执行错误,错误提示是这样的:
>>>1+'2'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and
'str'
但是在 JavaScript 中执行这段表达式,却是可以返回一个结果的,最终返回的结果是字符串“12”。
那么为什么同样的表达式,在 Python 和 JavaScript 中执行为什么会有不同的结果?为什么在 JavaScript 中执行,输出的是字符串“12”,不是数字 3 或者字符串“3”呢?
什么是类型系统 (Type System)?
在上边的表达式中,涉及到了两种不同类型的数据的相加。要想理清以上两个问题,我们就需要知道类型的概念,以及 JavaScript 操作类型的策略。
对机器语言来说,所有的数据都是一堆二进制代码,CPU 处理这些数据的时候,并没有类型的概念,CPU 所做的仅仅是移动数据,比如对其进行移位,相加或相乘。
而在高级语言中,我们都会为操作的数据赋予指定的类型,类型可以确认一个值或者一组值具有特定的意义和目的。所以,类型是高级语言中的概念。
在 JavaScript 中,你可以这样定义变量:
var num = 100 # 赋值整型变量
let miles = 1000.0 # 浮点型
const name = "John" # 字符串
V8 是怎么执行加法操作的?
了解了类型系统,接下来我们就可以来看看 V8 是怎么处理 1+“2”的了。当有两个值相加的时候,比如:
a+b
V8 会严格根据 ECMAScript 规范来执行操作。ECMAScript 是一个语言标准,JavaScript 就是 ECMAScript 的一个实现,比如在 ECMAScript 就定义了怎么执行加法操作,如下所示:
具体细节你也可以参考规范,我将标准定义的内容翻译如下:
- 把第一个表达式 (AdditiveExpression) 的值赋值给左引用 (lref)。
- 使用 GetValue(lref) 获取左引用 (lref) 的计算结果,并赋值给左值。
- 使用ReturnIfAbrupt(lval) 如果报错就返回错误。
- 把第二个表达式 (MultiplicativeExpression) 的值赋值给右引用 (rref)。
- 使用 GetValue(rref) 获取右引用 (rref) 的计算结果,并赋值给 rval。
- 使用ReturnIfAbrupt(rval) 如果报错就返回错误。
- 使用 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
执行这段代码,你觉得应该返回什么内容呢?
上面我们介绍过了,由于需要先使用 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"
你觉得执行这段代码会打印出什么内容呢?欢迎你在留言区与我分享讨论。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。