将值从一种类型转换为另一种类型通常称为类型转换,这是显示的情况;隐式的情况称为强制类型转换。
我们可以这样来区分:类型转换发生在静态类型语言的编译阶段,而强制类型转换则发生在动态类型语言的运行时。然而在 JavaScript 中通常将它们统称为强制类型转换,但我们可以用“隐式强制类型转换”和“显示强制类型转换”来区分。
一、抽象值转换
要了解 JavaScript 中的强制类型转换,我们需要掌握字符串、数字和布尔值之间的类型转换的基本规则。ES5 规范第 9 节中定义了一些“抽象操作”(即“仅供内部使用的操作”)和转换规则。
这里我们介绍一下 ToString、ToNumber、ToBoolean 和 ToPrimitive 。
1.1 ToString
抽象操作 ToString,它负责处理非字符串到字符串的强制类型转换。
对于基本类型值 - 字符串化规则为:
null
转换为"null"
;undefined
转换为"undefined"
;true
转换为"true"
;12
转换为"12"
;对于那些极小和极大的数字使用指数形式。
对于普通对象类型 - 字符串化规则为:
ToString()(Object.prototype.toString())
返回对象内部属性 [[Class]] 的值,如“[[object Object]]”;- 如果对象有自定义了
toString()
方法,字符串化时就会调用该方法并使用其返回值; - 数组的默认
toString()
方法经过了重新定义,将所有单元字符串化以后再用“,”连接起来; - 其它情况下将对象强制类型转换为 string 是通过 ToPrimitive 抽象操作来完成的。
1.2 ToNumber
抽象操作 ToNumber,它负责处理非数字值到数字值的强制类型转换。
对于基本类型值 - 数字化规则为:
true
转换为 1,false
转换为 0;undefined
转换为NaN
,null
转换为 0;''
、'/n'
、' '
等空字符串转换为 0;- 对于字符串的处理遵循数字常量的相关规则。处理失败时则返回
NaN
。
对于对象类型 - 数字化规则为:
- 对象(包括数组)首先会被转换为相应的基本类型值,如果返回的是非数字的基本类型,则再遵循以上规则将其强制转换为数字;
- 为了将值转换为相应的基本类型,会进行抽象操作 ToPrimitive。
1.3 ToBoolean
JavaScript 中的值可以分为一下两类:
- 假值:可以被强制类型转换为
false
的值 - 真值:其他(被强制类型转换为
true
的值)
1.3.1 假值(falsy)
JavaScript 规范具体定义了一小撮可以被强制类型转换为 false
的值。
一下这些是假值:
false
undefined
null
+0
、-0
和NaN
''
1.3.2 假值对象
浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些就是“假值对象”。假值对象看起来和普通对象并无二致(都有属性等),但将它们强制类型转换为布尔值时结果为 false
。
最常见的例子是 document.all
,它是一个类数组对象,包含了页面上的所有元素;
那为什么它要是假值呢?
在许多年前,程序员们通过将 document.all
强制类型转换为布尔值来判断浏览器是否是老版本的 IE,if(document.all) { /* it's IE */ }
仍然存在于许多程序中,但为了让新版本更符合标准,IE 不打算继续支持它,所以将它设置为假值。
1.3.3 真值(truthy)
真值就是假值列表之外的值。
1.4 ToPrimitive
将对象强制类型转换为基本类型,是通过 ToPrimitive 操作:
- 首先检查该值是否有
valueOf()
方法。如果有并且返回基本类型值,就使用该值进行强制类型转换; - 如果没有就使用
toString()
的返回值来进行强制类型转换; - 如果
valueOf()
和toString()
均不返回基本类型值,会产生TypeError
错误。
例如:将数组转换为基本类型
let arr1 = [1, 2]
let arr2 = [3, 4]
arr1 + arr2 = '1,23,4'
从上面的例子分析,数组的 valueOf()
方法无法得到基本类型的值,所以转到调用 toString()
方法。因此上例中的两个数组变成了 '1,2'
和 '3,4'
,+
将它们拼接为 '1,23,4'
。
二、显示强制类型转换
显示强制类型转换是那些显而易见的类型转换。
2.1 字符串和数字之间的显示转换
- 使用
String()
和Number()
强制转换; - 使用
toString()
转换为字符串; - 使用一元
+
运算符转换为数字; - 使用
parseInt()
和parseFloat()
解析数字字符串。
2.2 显示转换为布尔值
- 使用
Boolean()
强制转换; - 使用
!!
语法转换
三、隐式强制类型转换
隐式强制类型转换指的是那些隐蔽的强制类型转换。
虽然隐式强制类型转换会让代码变得晦涩难懂,但它的作用是减少冗余,让代码更简洁。
3.1 字符串和数字之间的隐式强制类型转换
常见的会产生隐式强制类型转换的操作有:
- 算数运算符,例如
+
运算符;+
运算符既能用于数字加法,也能用于字符串拼接。
例如:
let a = '42'
let b = '0'
let c = 42
let d = 0
let arr1 = [1, 2]
let arr2 = [3, 4]
a + b // '420'
c + d // 42
arr1 + arr2 // '1,23,4'
从上面的结果看,不同类型的值进行 +
运算,执行的规则各不相同,JavaScript 中相关的规范如下:
- 如果某个操作数是字符串或者能强制类型转换为字符串的话,
+
运算符将进行字符串拼接操作; - 如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,得到基本类型值后再根据以上规则进行操作;
- 简单来说就是,如果
+
的其中一个操作数是字符串(或者通过以上步骤可以得到字符串),则执行字符串拼接;否则执行数字加法。
3.2 隐式强制类型转换为布尔值
常见的会发生布尔值隐式强制类型转换的情况有:
if (...)
语句中的条件判断表达式;while (...)
和do...while(...)
循环中的条件判断表达式;for (...; ...; ...)
语句中的条件判断表达式(第二个);? :
(三元表达式)中的条件判断表达式;- 逻辑运算符
||
(逻辑或)和&&
(逻辑与)左边的操作数(作为条件判断表达式)。
以上情况中,非布尔值会被隐式强制类型转换为布尔值,遵循前面介绍的 ToBoolean 抽象操作规则。
3.3 && 和 ||
ES5 规范 11.11 节:
&& 和 || 运算符的返回值并不一定是布尔类型,而是两个操作数其中一个的值。
例如:
let a = 'ppg'
let b = 42
let c = null
a || b // 'ppg'
a && b // 42
c || a // 'ppg'
c && b // null
在 C 和 PHP 中,上例的结果是 true 或 false,在 JavaScript(以及 Python 和 Ruby) 中却是某个操作数的值。相关的规则如下:
&& 和 || 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断:
- 对于 && 来说,如果条件判断结果为 true,则返回第二个操作数的值;如果为 false,则返回第一个操作数的值。
- 对于 || 来说,如果条件判断结果为 true,则返回第一个操作数的值;如果为 false,则返回第二个操作数的值。
&& 和 || 返回它们其中一个操作数的值,而非条件判断的结果。
因此,在 if(...)
等条件判断语句中的类似 if (a && b || c)
的表达式,在 && 和 || 操作得到最终的结果后,会执行布尔值的隐式强制类型转换。
3.4 符号的强制类型转换
ES6 中引入了符号类型,它的强制类型转换有一些坑,这里需要注意下。
ES6 允许从符号到字符串的显示强制类型转换,然而隐式强制类型转换会产生错误。
例如:
let str = Symbol('hello')
String(str) // 'Symbol(hello)'
str + '' // TypeError
符号不能被强制类型转换为数字(显示和隐式都会产生错误),但可以被强制类型转换为布尔值(显示和隐式的结果都为 true)。
由于规则缺乏一致性,我们要对 ES6 中符号的强制类型转换要多加小心。
四、宽松相等和严格相等
宽松相等 == 和严格相等 === 都用来判断两个值是否“相等”,它们的区别是:
- 常见的解释是“== 检查值是否相等,=== 检查值和类型是否相等”,然而这样还不够准确;
- 正确的解释是:“== 允许在相等比较中进行强制类型转换,而 === 不允许”。
4.1 抽象相等
ES5 规范 11.9.3 节的“抽象相等比较算法”定义了 == 运算符的行为:
4.1.1 字符串和数字之间的相等比较
ES5 规范定义如下:
- 如果 Type(x) 是数字,Type(y) 是字符串,则返回 x == ToNumber(y) 的结果。
- 如果 Type(x) 是字符串,Type(y) 是数字,则返回 ToNumber(x) == y 的结果。
例如:
let x = 42
let y = '42'
x == y // true
4.1.2 其它类型和布尔值之间的相等比较
ES5 规范定义如下:
- 如果 Type(x) 是布尔类型,则返回 ToNumber(x) == y 的结果。
- 如果 Type(y) 是布尔类型,则返回 x == ToNumber(y) 的结果。
例如:
let x = '42'
x == true // false
x == false // false
从上例结果来看,字符串 '42'
既不等于 true,也不等于 false。虽然 '42'
是一个真值,但是 '42' == true
中并没有发生布尔值的比较和强制类型转换。这里并不涉及 ToBoolean,所以 '42'
是真值还是假值与 == 本身没有关系!
4.1.3 null 和 undefined 之间的相等比较
ES5 规范定义如下:
- 如果 x 为 null,y 为 undefined,则结果返回 true。
- 如果 x 为 undefined,y 为 null,则结果返回 true。
例如:
let x = null
let y = undefined
x == y // true
x == 0 // false
y == false // false
在 == 中 null 和 undefined 相等(它们与其自身也相等),除此之外其他值都不存在这种情况。
4.1.4 对象和非对象之间的相等比较
ES5 规范定义如下:
- 如果 Type(x) 是字符串或数字,Type(y) 是对象,则返回 x == ToPrimitive(y) 的结果。
- 如果 Type(x) 是对象,Type(y) 是字符串或数字,则返回 ToPrimitive(x) == y 的结果。
(Tips:这里只提到了字符串和数字,没有布尔值。是因为上面介绍的规则中规定了布尔值会先被强制类型转换为数字)
例如:
let x = 42
let y = [42]
x == y // true
4.1.5 比较少见的情况
特殊场景:
'0' == false // true
false == 0 // true
false == '' // true
false == [] // true
'' == 0 // true
'' == [] // true
0 == [] // true
[] == ![] // true
0 == '/n' // true
安全运用隐式强制类型转换
我们要对 == 两边的值认真推敲,以下两个原则可以让我们有效避错:
- 如果两边的值中有
true
或false
,则不要使用 ==。 - 如果两边的值中
[]
、''
或者0
,则不要使用 ==。
此时最好用 === 来避免不经意的强制类型转换。
参考连接
- 《你不知道的Javascript(中卷)》