运算与转换:一步一步推导结果
“JavaScript 很奇怪”常常只是把几步转换压缩成了一个结果。本节把运算符拆成输入类型、转换步骤和返回值。默认在数据进入程序时明确转换,阅读旧代码时仍要能解释隐式转换。
先区分算术、拼接与赋值
console.log(7 + 2); // 9
console.log('7' + 2); // 72
console.log('7' - 2); // 5
console.log(2 ** 3); // 8
console.log(-7 % 3); // -1+ 在完成原始值转换后,只要任一侧是字符串,就走字符串拼接;-、*、/ 等走数值运算。% 是余数运算,负数结果不等同于某些数学定义中的非负模。需要环形位置时常用 ((n % size) + size) % size,前提是 size 为正。
n += 1 是复合赋值;它也服从 + 的拼接规则。n++ 返回增加前的值,++n 返回增加后的值。若返回值并不需要,单独写 n += 1 更容易看懂。
优先级决定表达式如何分组,不等于随意调换副作用执行顺序。乘除高于加减;** 右结合,2 ** 3 ** 2 是 2 ** (3 ** 2)。负底数明确写 (-2) ** 2,-2 ** 2 是语法错误。
显式转换不是校验
console.log(Number(' 12 ')); // 12
console.log(Number('')); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number('12px')); // NaN
console.log(parseInt('12px', 10)); // 12
console.log(parseFloat('2.5rem')); // 2.5Number 尝试转换整个文本;parseInt、parseFloat 从开头读可解析前缀,后面还有字符也可能成功。读取 CSS 长度时前缀解析可能有意义;验证数量字段时,它会把 '12本' 也接受成 12。parseInt 显式传进制,例如二进制用 2;不要把它当通用截断小数函数。
String(value) 产生文本,Boolean(value) 判断真假,不会理解 'false' 的自然语言意思。下一篇会用 Number.isFinite、Number.isInteger 等把转换后的合法性检查补齐。
解析数字会先把输入当文本读
console.log(Number('3e2')); // 300
console.log(parseInt('3e2', 10)); // 3
console.log(parseFloat('3e2')); // 300
console.log(parseInt(1e21, 10)); // 1
console.log(Math.trunc(1e21)); // 1e+21前三行分别回答不同问题:整个文本能否表示一个数、开头能读出多少整数位、开头能读出多少浮点表示。parseInt 在e处停止,parseFloat 认识指数形式。最后两行更值得注意:parseInt 接到Number后先转字符串,1e21 的文本形式含指数,于是只读出开头的1。它从来没有承诺“把任何数字去掉小数部分”。
所以不要根据“对12.8试了一次能得到12”就宣布parseInt可替代截断。选择API时必须问它接收的模型:是数字,还是有数字前缀的文本?这比收集更多偶然成功的例子更可靠。
三类相等比较解决不同问题
| 比较 | 转换类型 | NaN 与自身 | +0 与 -0 |
|---|---|---|---|
=== | 不转换 | 不等 | 相等 |
Object.is | 不转换 | 相等 | 不等 |
| SameValueZero | 不转换 | 相等 | 相等 |
SameValueZero 是集合和部分方法使用的内部规则,没有同名全局函数。第三章会看到 includes、Map 和 Set 使用它。
== 按类型组合决定转换,不是“先把两边全转为数字”。例如 null == undefined 为真,但 null == 0 为假;布尔值会先转数值,字符串与数值比较会转数值;两个对象仍比较身份。
console.log(false == '0'); // true:false → 0,'0' → 0
console.log(null == 0); // false:空值有专门规则
console.log(null == undefined); // true
console.log('10' < '2'); // true:两个字符串按码元比较
console.log('10' < 2); // false:这里是数值比较日常用 === 让比较意图清楚。value == null 可作为刻意同时判断两种空值的写法,但不必把所有比较都改成宽松比较。NaN 参与 <、>、<=、>= 都为假,不能由“不是大于”推出“小于等于”。
从类型对出发走相等分支
推导 false == '0' 可以写成三步:
boolean 与 string → boolean先变Number
0 与 '0' → string按数值转换
0 与 0 → 同类型严格相等,结果true但 null == 0 在“空值与其他类型”的分支已经得到false,不会继续强行把null转成0。某个值能被Number转换,不代表每个运算符都会要求它转换。 同理 null >= 0 是true,关系运算采用了自己的数值比较路径;不能从它反推 null == 0 必须为true。
console.log(null >= 0, null == 0); // true false
console.log(2n == '2', 2n == '2.0'); // true false
console.log(2n < 2.5); // trueBigInt与字符串的宽松比较尝试按整数文本解释,'2.0' 不是可接受的BigInt文本,比较为false;它不会先按Number解析再截断。BigInt与Number比较大小可以比较数学值,但算术相加仍禁止直接混合。比较规则与算术规则必须各自查证。
再看NaN:x > NaN为false,却不表示 x <= NaN为true。前者取反得到true,后者独立比较仍得到false。“不是大于就一定小于等于”需要比较关系满足正常数值的前提,NaN打破了这个前提。
逻辑运算符会返回操作数
console.log('ready' && 8); // 8
console.log('' && 8); // 空字符串
console.log('ready' || 8); // ready
console.log(false ?? 8); // false&& 在左边为假时直接返回左边,否则求右边;|| 在左边为真时直接返回左边;?? 只把空值视为需要替补。这叫短路,右边可能根本不执行。! 总返回布尔值,!!value 等价于常规布尔转换。
对应的 &&=、||=、??= 只在满足条件时赋值。它们对左侧求值一次,涉及属性访问时不应简单理解成机械文本替换。?? 与 ||、&& 直接混写需要括号:(name || fallback) ?? '匿名'。
短路改变的不只是结果,还有执行次数
下面不使用尚未学习的函数,直接用赋值观察右侧是否运行:
let attempts = 0;
const first = '已找到' || (attempts += 1);
const second = false && (attempts += 1);
const third = 0 ?? (attempts += 1);
console.log(first, second, third, attempts); // 已找到 false 0 0
const fourth = null ?? (attempts += 1);
console.log(fourth, attempts); // 1 1前三次左侧已经决定结果,因此右侧根本没有求值。第四次需要替补值,才把attempts增加。以后右侧若是网络请求、缓存生成或函数调用,同样可能完全不发生。
因此 a && b() 适合“a为真时执行b”的明确意图,却不能无条件替代 if:它的结果可能是a本身,而不是布尔值,也不能直接包含一串声明与多步动作。为了省一行改变了返回值或副作用次数,往往得不偿失。
对象转换与可选链留意前置
这里先认识边界:数组、对象参与运算可能先变成原始值;普通对象一般通过 valueOf、toString,也可以定义 Symbol.toPrimitive。例如空数组默认文本为 '',所以 [] == 0 可推导为 '' == 0,再成为 0 == 0。但 对象的布尔判断不执行这条转换链,所以 Boolean([]) 为真。对象定制转换在第四章继续。
record?.name 在 record 是空值时返回 undefined,否则正常读取属性;它不能掩盖一个根本没声明的变量。record?.method() 和 record.method?.() 检查的位置不同,也不保证存在的值可调用。第三章用真实对象示例展开。
位运算 & | ^ ~ << >> >>> 针对 Number 通常进入 32 位整数转换,不能替代所有整数运算。BigInt 有不同边界,下一篇会讲。逗号运算符依次求值并返回最后一项;void 求值后返回 undefined;delete 删除对象属性。知道它们的职责比把它们塞进一条表达式更重要。
练习
逐步解释
'' == 0、'' === 0、'5' + 1、'5' - 1。数量允许为 0,缺失时默认 1。分别用
||和??处理 0、undefined,比较结果。逐步解释parseInt(1e21, 10)为何是1;null >= 0和null == 0为什么不同;短路右侧的赋值是否总会执行?
阅读器有5页,位置编号为0~4,当前在0页。按“上一页”应回到4,连续按7次“上一页”应到3。用本节的余数规则写表达式,并测试移动0页、移动-5页;这里移动量约定为整数,不使用尚未学习的函数。