JavaScript 数值精度、NaN 和安全整数怎么理解?
简化版
JavaScript 的普通数字使用 IEEE 754 双精度浮点数表示,所以整数、小数都走同一种 Number。这会带来小数精度误差、安全整数上限、NaN 不等于自身等问题。
常见判断:用 Number.isNaN 判断真正的 NaN,用 Number.isFinite 判断有限数字,用 Number.isSafeInteger 判断是否在安全整数范围内。金额计算不应直接依赖浮点小数。
详细版
经典例子:
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(NaN === NaN); // false
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.isSafeInteger(9007199254740992)); // false
小数误差来自二进制无法精确表示某些十进制小数。0.1、0.2 在二进制中都是无限循环,只能近似存储。
金额常见做法是用整数分、定点数库或后端精确计算:
const priceCent = 1999;
const count = 3;
console.log(priceCent * count); // 5997 分
isNaN 会先做类型转换,Number.isNaN 不会,面试中更推荐后者。
完整版教学
一、Number 的底层模型
JavaScript 的 Number 是双精度浮点数,不区分 int、float、double。
它大致由三部分组成:
sign | exponent | fraction
符号 | 指数 | 尾数
这让它能表示很大和很小的数,但不能精确表示所有十进制小数。
数值精度题不要只背
0.1 + 0.2,要说清楚“十进制小数转二进制时可能无限循环,只能近似存储”。
二、为什么 0.1 + 0.2 不等于 0.3
十进制的 0.1 转成二进制是无限循环小数,存进固定长度尾数时会被截断或舍入。
const sum = 0.1 + 0.2;
console.log(sum); // 0.30000000000000004
console.log(Math.abs(sum - 0.3) < Number.EPSILON); // true
Number.EPSILON 表示 1 附近两个可表示数之间的差距,可以用于小范围浮点比较。
三、安全整数是什么
安全整数范围是 -(2^53 - 1) 到 2^53 - 1。
| 常量 | 值 |
|---|---|
Number.MAX_SAFE_INTEGER | 9007199254740991 |
Number.MIN_SAFE_INTEGER | -9007199254740991 |
超过这个范围,整数不一定能逐一精确表示。
console.log(9007199254740992 === 9007199254740993); // true
这就是订单号、雪花 ID、数据库大整数常转字符串的原因。
四、NaN 的判断方式
NaN 表示不是一个有效数字,它有一个特殊性质:不等于自身。
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
全局 isNaN 会做类型转换。
isNaN("abc"); // true
Number.isNaN("abc"); // false
如果要判断一个值本身就是 NaN,优先用 Number.isNaN。
五、Infinity 和有限数字判断
除以 0 可能得到 Infinity 或 -Infinity。
console.log(1 / 0); // Infinity
console.log(Number.isFinite(1 / 0)); // false
全局 isFinite 也会做类型转换,因此更推荐 Number.isFinite。
isFinite("123"); // true
Number.isFinite("123"); // false
六、业务计算怎么规避问题
金额、库存、积分这类业务不应直接用浮点小数做最终结算。
可选方案:
- 用整数单位,例如金额用分
- 使用 decimal / big number 库
- 大整数 ID 用字符串传输
- 浮点展示时只在显示层格式化
const amount = 0.1 + 0.2;
console.log(amount.toFixed(2)); // "0.30",只是格式化显示
toFixed 不是精确计算方案,它只是把结果格式化成字符串。
七、常见误区与追问
- 误区:JavaScript 只有小数才有精度问题。 超过安全整数范围的整数也会有精度问题。
- 误区:
toFixed可以解决金额计算。 它主要用于展示格式化,不应替代精确计算。 - 误区:
isNaN和Number.isNaN完全一样。 前者会类型转换,后者更严格。 - 误区:
NaN === NaN应该为 true。NaN按规范不等于自身。 - 追问:为什么接口 ID 经常用字符串? 因为前端 Number 无法安全承载所有大整数。
- 追问:浮点数比较应该怎么做? 可以判断差值是否小于可接受误差,而不是直接
===。
八、加强记忆
记住三组 API:
NaN -> Number.isNaN
Infinity -> Number.isFinite
big int -> Number.isSafeInteger
回答时先讲 Number 的浮点模型,再给 0.1 + 0.2 和安全整数两个数字例子,最后落到业务:金额用整数或精确库,大整数 ID 用字符串,展示格式化和计算精度要分开。