JavaScript 数字类型

深入解析 IEEE 754 双精度浮点数、0.1 + 0.2 !== 0.3 的原因、安全整数范围、NaN 和 Infinity 的产生场景,以及 BigInt 处理大整数的方法。

为什么理解 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() 可以检查一个整数是否在安全范围内。


延展阅读