JavaScript类型转换(三)——转换方向和时机

这是JavaScript类型转换系列文章的第三篇,讲述发生转换的方向和时机。

转换方向和时机

通过上一篇内容,读者应该会发现在掌握转换规则后只要确定转换方向就可以得到转换结果了,实际上也是如此,但难点正是让人迷惑的发生时机和转换方向。本篇是类型转换的重点。

显式类型转换

存在三个强制类型转换的函数:String()Number()Boolean(),已经很明确的表明了转换的方向。Object()把内容转换为引用类型。

引用类型的toString()函数,名称已经表达了强烈的转换方向,需要注意的是3个基本类型numberstringboolean虽然不是引用类型,但可以通过自动装箱使用这个函数。

全局的parseInt函数,目的是把字符串转换为整数。严格意义上说,这个函数并不是类型转换的作用,因为它会舍弃小数部分。

全局的parseFloat函数,目的是把字符串转换为浮点数。

下面用一个表格小结一下:

转换函数 转换方向 适用类型 举例或说明
String() 向字符串 所有类型
toString() 向字符串 引用类型和3个基本类型 (16).toString()->"16"
Number() 向数字 所有类型
Object() 向引用类型 所有类型
parseInt() 向数字 字符串 参数要求是字符串,否则会触发隐式转换
parseFloat() 向数字 字符串 同上
Boolean() 向布尔 所有类型

隐式类型转换

不是程序员有意为之但实际发生了的类型转换称为隐式类型转换,之所以要悄悄的做这些,就是为了提高编码的宽容度,让语言使用简化,但现在副作用更大。通常在运算和函数调用会发生,以下是各种情况:

算数运算

  • 位运算~&|^<<>>>>>
  • 其他算数运算
    • 一元-+
    • */%**
    • 二元+操作数中没有字符串

这些场景操作数不是数字的都会发生向number的隐式类型转换。

BigInt不能与Number混合计算,所以不会有隐式类型转换

字符串操作

  • 模版字符串中插入的值会被转换为字符串
  • 二元+运算,只要操作数有一个是字符串,所有操作数都要转换为字符串(包括BigInt

关系比较

><>=<===!=

两侧内容不同为字符串或不同为数字,两侧会发生向number的隐式类型转换。

引用类型发生类型转换,遵循ToPrimitive流程,转换倾向于number,得到转换结果后再根据上一条规则进行类型转换。

相等不等

==!=的操作内容都是引用类型,不会发生类型转换,实际判断的是二者是否为同一引用。

特殊值的比较结果

  • NaN不与任何内容相等,包括它自身
  • nullundefined不与除了对方和自身之外的任何内容相等
  • 0-0相等

逻辑运算

  • 逻辑运算
    • !:强制做布尔转换后取反
    • &&||:左侧内容会被强制做布尔转换

&&|| 表达式的值不一定是布尔值,在发生短路后,返回的是右侧原值。

条件判断

诸如:ifforwhiledo...while、三元表达式这类语句和表达式的条件部分,会发生向布尔类型的转换。

上述逻辑运算的结果即使不是布尔值,在这种场景下也会发生向布尔类型的转换。

引用类型的属性名

引用类型的属性名只能是stringSymbol,定义或访问属性的时候类型不符合会发生隐式类型转换。

例如:
obj.name等效于 obj['name']

{9n:'nine'}等效于{9:'nine'}{'9':'nine'}

数组索引

数组的索引是数组对象的属性名,所以数字不能作为数组索引。但是我们已经习惯了数字,这个过程也发生了隐式类型转换。

参数类型适配

这种情况是参数有类型限制,实参不符合该限制,会触发隐式类型转换。
比如:alretparseIntparseFloatJSON.stringify()

运算的顺序

为什么研究类型转换还要考虑运算符执行顺序?

先执行的运算会先触发对应的类型转换规则,进而影响后续运算的转换逻辑。

举几个例子供思考:

1
2
3
const result1 = +"1" + 2;
const result2 = '0'&&1>6;
const result3 = true||''&&1;

搞清楚运算的执行顺序:运算符的优先级和结合性

运算符的优先级

运算符的优先级规定了不同运算符执行的先后顺序,优先级高的运算符先执行。

比如以下代码中乘法的优先级高于加法,优先执行乘法运算:

1
4 + 5 * 6 // 34

运算符的结合性

运算符的结合性规定了优先级相同的运算符执行的顺序,多数运算符都是左结合的,极少数为右结合。

比如幂运算(**)为右结合,以下代码会从右向左执行:

1
2
3
4
2 ** 3 ** 2
// 相当于 2 ** (3 ** 2)
// 512

主要运算符优先级和结合性

说明:结合性以左结合为主,所以省略,突出占少数的右结合情况。

优先级 运算符类型 结合性 运算符 示例
21 分组 (...) (a + b)
20 成员访问 . obj.property
20 需计算的成员访问 [...] obj["property"]
20 函数调用 (...) func()
20 new(带参数列表) new ...(...) new Date()
19 new(无参数列表) 右结合 new ... new Date
18 后置递增 ...++ a++
18 后置递减 ...-- a--
17 逻辑非 右结合 ! !true
17 按位非 右结合 ~ ~a
17 一元加 右结合 + +"5"
17 一元减 右结合 - -a
17 前置递增 右结合 ++... ++a
17 前置递减 右结合 --... --a
17 typeof 右结合 typeof typeof a
17 void 右结合 void void 0
17 delete 右结合 delete delete obj.prop
17 await 右结合 await await promise
16 右结合 ** 2 ** 3
15 * a * b
15 / a / b
15 取模 % a % b
14 + a + b
14 - a - b
13 按位左移 << a << 2
13 按位右移 >> a >> 2
13 无符号右移 >>> a >>> 2
12 小于 < a < b
12 小于等于 <= a <= b
12 大于 > a > b
12 大于等于 >= a >= b
12 in in "prop" in obj
12 instanceof instanceof obj instanceof Class
11 相等 == a == b
11 不相等 != a != b
11 全等 === a === b
11 不全等 !== a !== b
10 按位与 & a & b
9 按位异或 ^ a ^ b
8 按位或 | a | b
7 逻辑与 && a && b
6 逻辑或 || a || b
5 空值合并 ?? a ?? b
4 条件运算符 右结合 ... ? ... : ... a ? b : c
3 赋值 右结合 = a = b
3 复合赋值 右结合 += -= *= /= a += b
2 yield 右结合 yield yield value
2 yield* 右结合 yield* yield* generator
1 逗号 , a, b, c

解析前面的举例

1
2
3
4
5
6
const result1 = +"1" + 2;//
// 一元加运算优先级高于普通加运算,会把字符串1转换为数字1
const result2 = '0'&&1>6;// false
// 小于运算符优先级高于逻辑与
const result3 = true||''&&1;//true
// 逻辑与优先级高于逻辑或

总结

显式类型转换比较简单,调用函数就是转换的时机,函数本身就决定了转换的方向。

隐式类型转换情况复杂,设计的初衷就是让一些本来类型敏感的操作更容易进行,在复杂运算的时候,还需要了解运算顺序。

表达式 8 / 4 / 2 的结果是?(提示:除法的结合性)
表达式 true || false && false 的结果是?(提示:&& 与 || 优先级)
表达式 3 > 2 && 2 > 1 || 1 > 0 的结果是?

let x = 2, y = 3;
let result = x++ + –y;

表达式 2 3 2 的结果是?(提示:** 的结合性)

let a = 1;
a += a * 2;

表达式 3 & 1 || 2 的结果是?(提示:按位与 & 与逻辑或 || 优先级)

表达式 ‘5’ + 3 * 2 的结果是?(提示:+ 作为字符串拼接的优先级)

let result = 1 ? 2 : 3 ? 4 : 5;

表达式 10 % 3 * 2 的结果是?

let x = (1 + 2, 3 + 4, 5 + 6);

表达式 !3 + 1 的结果是?(提示:逻辑非 ! 的优先级)

表达式 3 << 1 + 1 的结果是?(提示:位移运算符与加法优先级)

表达式 null == undefined && 0 === false 的结果是?

参考内容

ToPrimitive