为什么理解 JavaScript 数字类型至关重要
JavaScript 的 number 类型是初学者最容易踩坑的地方之一。0.1 + 0.2 !== 0.3 这个经典问题难倒过无数开发者。但这只是冰山一角——精度丢失、安全整数范围、进制转换,每一处细节都可能埋藏着 bug。
理解 JavaScript 数字的底层表示,不仅能帮你避免这些坑,还能在需要处理金融计算、精确度量时做出正确的技术决策。
一、IEEE 754 双精度浮点数
1.1 什么是 IEEE 754
JavaScript 使用 IEEE 754 标准存储数字——具体是 64 位双精度浮点数。这意味着所有数字(包括整数)在内部都是浮点数表示。
flowchart LR
subgraph IEEE754["64-bit Double Precision"]
S["符号位<br/>Sign: 1bit"]
E["指数<br/>Exponent: 11bits"]
M["尾数<br/>Mantissa: 52bits"]
end
S --> R["值 = (-1)^S × 1.M × 2^(E-1023)"]
组成部分:
- 符号位(Sign):1 位,0 表示正,1 表示负
- 指数(Exponent):11 位,决定数值范围
- 尾数(Mantissa):52 位,决定精度
1.2 浮点数精度问题
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
原因:十进制的 0.1 和 0.2 在二进制中是无限循环小数,存储时必须截断,导致精度丢失。
// 十进制 0.1 的二进制表示
// 0.0001100110011001100110011001100110011001100110011... (无限循环)
// 存储时只能保留 52 位尾数,末尾需要舍入
// 实际存储的近似值:
// 0.1 ≈ 0.1000000000000000055511151231257827021181583404541015625
// 0.2 ≈ 0.200000000000000011102230246251565404236316680908203125
// 相加后 ≈ 0.3000000000000000444089209850062616169452667236328125
1.3 解决方案:误差容忍
// 方案一:使用误差范围比较
function withinTolerance(a, b, epsilon = 1e-10) {
return Math.abs(a - b) < epsilon;
}
console.log(withinTolerance(0.1 + 0.2, 0.3)); // true
// 方案二:转换为整数计算
function addIntegers(a, b, decimals = 1) {
const factor = Math.pow(10, decimals);
return (a * factor + b * factor) / factor;
}
console.log(addIntegers(0.1, 0.2, 1)); // 0.3
// 方案三:使用 Decimal.js 等库
// import Decimal from 'decimal.js';
// new Decimal('0.1').plus('0.2').equals('0.3') // true
二、安全整数范围
2.1 安全整数定义
IEEE 754 可以精确表示连续的整数,从 -(2^53 - 1) 到 2^53 - 1。这个范围叫做安全整数范围。
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991 (2^53 - 1)
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991
// 安全范围内的运算
console.log(9007199254740991 + 1); // 9007199254740992
console.log(9007199254740991 + 2); // 9007199254740992 ← 精度丢失!
// 验证
console.log(9007199254740991 + 1 === 9007199254740991 + 2); // true ← 问题
2.2 为什么是 2^53 - 1?
尾数 52 位,加上隐含的 1 位(规范化二进制科学计数法),共 53 位有效数字。
// 2^53 的二进制:1 后面跟 53 个 0
// 2^53 - 1 的二进制:53 个 1
// 这是 IEEE 754 能精确表示的最大连续奇数
2.3 isSafeInteger
function safeAdd(a, b) {
if (!Number.isSafeInteger(a) || !Number.isSafeInteger(b)) {
throw new Error('Unsafe integer detected');
}
const result = a + b;
if (!Number.isSafeInteger(result)) {
throw new Error('Result would be unsafe');
}
return result;
}
safeAdd(1, 2); // 3
safeAdd(Number.MAX_SAFE_INTEGER, 1); // Error: Result would be unsafe
三、NaN 与 Infinity
3.1 NaN 的产生场景
// 数学运算产生 NaN
console.log(Math.sqrt(-1)); // NaN
console.log(0 / 0); // NaN
console.log(parseInt("hello")); // NaN
console.log(undefined + 1); // NaN
console.log("abc" * 1); // NaN
// NaN 是唯一不等于自身的值
console.log(NaN === NaN); // false
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(Object.is(NaN, NaN)); // true — ES2015
3.2 Infinity 的产生场景
console.log(1 / 0); // Infinity
console.log(-1 / 0); // -Infinity
console.log(Math.pow(2, 1024)); // Infinity (超过最大值)
console.log(Number.MAX_VALUE); // 1.7976931348623157e+308
// 运算中的 Infinity
console.log(Infinity + 1); // Infinity
console.log(Infinity - Infinity); // NaN
console.log(Infinity * Infinity); // Infinity
console.log(Infinity * -1); // -Infinity
console.log(10 / Infinity); // 0
3.3 isFinite vs isNaN
// isFinite:检查是否是有限数值(排除 Infinity, -Infinity, NaN)
console.log(isFinite(42)); // true
console.log(isFinite(Infinity)); // false
console.log(isFinite(NaN)); // false
console.log(isFinite("42")); // true — 会强制转换
// Number.isFinite:不强制转换
console.log(Number.isFinite(42)); // true
console.log(Number.isFinite("42")); // false
console.log(Number.isFinite(Infinity)); // false
// isNaN vs Number.isNaN
console.log(isNaN(NaN)); // true
console.log(isNaN("hello")); // true — 强制转换
console.log(Number.isNaN("hello")); // false — 不强制转换
四、BigInt
4.1 为什么需要 BigInt
当整数超出安全范围时,JavaScript 无法精确表示。BigInt 解决了这个问题。
const big = 9007199254740993n; // n 后缀表示 BigInt
console.log(big); // 9007199254740993n
console.log(typeof big); // "bigint"
// 安全整数运算 vs BigInt 运算
console.log(9007199254740991 + 2 === 9007199254740991 + 2); // true (错误的结果)
console.log(9007199254740991n + 2n === 9007199254740991n + 2n); // false (正确的结果)
4.2 BigInt 运算
const a = 123456789012345678901234567890n;
const b = 987654321098765432109876543210n;
console.log(a + b); // 1111111110111111110111111111200n
console.log(a * b); // 1219326311370217952261850325335204699668692360n
console.log(a - b); // -864197532086419753218641543320n
// 除法会截断小数部分
console.log(10n / 3n); // 3n
// 不能与 number 混合运算
// console.log(1n + 1); // TypeError
console.log(1n + BigInt(1)); // 2n
4.3 BigInt 转换
const num = 42;
const big = 42n;
// number → BigInt
BigInt(num); // 42n
Number(big); // 42
// string → BigInt
BigInt("123456789"); // 123456789n
// BigInt → string
big.toString(); // "42"
big.toString(16); // "2a" (十六进制)
五、进制转换
5.1 parseInt 与 Number 的区别
// parseInt:解析字符串开头为指定进制的整数
console.log(parseInt("42")); // 42
console.log(parseInt("42px")); // 42 — 遇到非数字字符停止
console.log(parseInt("0xFF")); // 255 — 自动识别十六进制
console.log(parseInt("1010", 2)); // 10 — 指定二进制
// 陷阱:超过 2^53 的整数会丢失精度
parseInt("9007199254740993"); // 9007199254740992 — 精度丢失!
// Number:全局转换,更严格
console.log(Number("42")); // 42
console.log(Number("42px")); // NaN — 不容忍非数字字符
console.log(Number("0xFF")); // 255
console.log(Number("1010")); // 1010
console.log(Number("1010", 2)); // 1010 — 不识别进制参数
5.2 toString 与 toRadixString
const num = 255;
// 十进制转其他进制
console.log(num.toString(2)); // "11111111"
console.log(num.toString(16)); // "ff"
console.log(num.toString(8)); // "377"
// 负数的进制转换
(-255).toString(16); // "-ff"
(-255).toString(2); // "-11111111"
// toFixed vs toPrecision
console.log((123.456).toFixed(2)); // "123.46"
console.log((123.456).toPrecision(4)); // "123.5"
5.3 常用场景
// RGB 转十六进制颜色
function rgbToHex(r, g, b) {
return '#' + [r, g, b]
.map(x => x.toString(16).padStart(2, '0'))
.join('');
}
console.log(rgbToHex(255, 128, 64)); // "#ff8040"
// 十六进制颜色转 RGB
function hexToRgb(hex) {
const match = hex.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
return match
? {
r: parseInt(match[1], 16),
g: parseInt(match[2], 16),
b: parseInt(match[3], 16)
}
: null;
}
六、面试高频问题
Q: 为什么 0.1 + 0.2 !== 0.3?
IEEE 754 双精度浮点数中,十进制的 0.1 和 0.2 在二进制是无限循环小数,存储时必须截断。截断引入的误差在相加后仍然存在,导致结果不是精确的 0.3。实际输出是 0.30000000000000004。解决方案包括使用误差容忍比较、转换为整数计算,或使用 Decimal.js 等高精度库。
Q: 什么是安全整数范围?
安全整数范围是 -(2^53 - 1) 到 2^53 - 1,即 -9007199254740991 到 9007199254740991。在这个范围内,IEEE 754 可以精确表示每一个整数。超出这个范围后,由于尾数位数限制,某些整数无法精确表示。Number.isSafeInteger() 可以检查一个整数是否在安全范围内。