在 JavaScript 中,数字 (Numbers) 是最基础且使用最频繁的数据类型之一。然而,与其他编程语言(如 Java, C++)不同,JavaScript 不区分整数、浮点数、双精度数等多种数字类型。它只有一个统一的数字类型,即遵循 IEEE 754 标准的双精度 64 位浮点数 (double-precision 64-bit floating-point format)

这种单一的数字类型带来了极大的灵活性,使得我们无需担心类型转换就能进行各种数学运算。但同时,它也引入了一些特有的行为和潜在的精度问题,特别是当涉及到大整数或精确浮点数运算时。理解 JavaScript 数字的底层工作原理、它的特殊值、提供的内置方法以及如何处理常见的计算陷阱,是编写健壮且高性能 JavaScript 应用的关键。

本教程将全面深入地探讨 JavaScript 数字的方方面面。我们将从介绍其统一的数字类型IEEE 754 标准开始,解释这种设计选择带来的优势与挑战。随后,我们将详细剖析 JavaScript 中各种数字字面量的表示方式,并深入理解三个特殊的数字值:Infinity-InfinityNaN。我们将探讨 Number 对象作为包装类型和作为静态工具类所提供的常量和方法,包括类型检查、字符串解析与格式化等。此外,本教程将重点讲解浮点数精度问题的根源及其常见的解决方案。最后,我们将简要介绍 ES2020 引入的 BigInt 类型,它为处理任意精度的整数提供了新的途径,并提供最佳实践和常见陷阱,帮助您彻底掌握 JavaScript 中的数字,编写出更精确、更可靠的数值处理代码。


1. JavaScript 数字的统一类型:IEEE 754 双精度浮点数

JavaScript 中所有的数字,无论是整数还是带小数的数,都统一表示为 IEEE 754 双精度 64 位浮点数。这意味着:

  • 没有独立的整数类型:103.14 在底层都是以浮点数的形式存储的。
  • 64 位存储: 使用 64 位来存储一个数字,其中:
    • 1 位用于符号 (sign)。
    • 11 位用于指数 (exponent)。
    • 52 位用于尾数 (mantissa / significand)。

这种表示方式决定了 JavaScript 数字的表示范围精度

1.1 表示范围

JavaScript 能够表示的最大的有限数值是 Number.MAX_VALUE (约为 1.7976931348623157e+308),最小的正数是 Number.MIN_VALUE (约为 5e-324)。超出这些范围的数值将自动转换为 Infinity0

1.2 整数精度

由于只有 52 位用于存储尾数(有效数字),JavaScript 可以安全地表示介于 -2^53 - 12^53 - 1 之间的所有整数。这个范围对应于 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER

  • Number.MAX_SAFE_INTEGER 的值为 9007199254740991
  • Number.MIN_SAFE_INTEGER 的值为 -9007199254740991

在这个“安全”整数范围之外,JavaScript 仍然可以表示更大的整数,但它们可能无法精确表示,可能会出现精度丢失。

console.log(Number.MAX_SAFE_INTEGER);         // 9007199254740991
console.log(Number.MAX_SAFE_INTEGER + 1);     // 9007199254740992 (依然精确)
console.log(Number.MAX_SAFE_INTEGER + 2);     // 9007199254740992 (!!! 精度丢失,结果与 +1 相同)

2. 创建数字

2.1 数字字面量 (Literals)

这是创建数字最常见和推荐的方式。

  • 整数:
    let integer1 = 10;
    let integer2 = -50;
    let zero = 0;
    
  • 浮点数 (小数):
    let float1 = 3.14;
    let float2 = -0.5;
    let float3 = .25; // 等同于 0.25
    
  • 科学计数法 (Exponential Notation):
    let bigNum = 1.23e5;   // 1.23 * 10^5 = 123000
    let smallNum = 4.5e-3; // 4.5 * 10^-3 = 0.0045
    
  • 其他进制表示:
    let hex = 0xFF;    // 十六进制 (255)
    let oct = 0o377;   // 八进制 (255, ES6 语法)
    let bin = 0b11111111; // 二进制 (255, ES6 语法)
    
2.2 Number() 构造函数 (或作为类型转换函数)
  • 作为类型转换函数 (推荐):Number() 作为函数被调用时(不带 new 关键字),它会尝试将传入的参数转换为数字类型。如果转换失败,则返回 NaN

    console.log(Number("123"));       // 123
    console.log(Number("3.14"));      // 3.14
    console.log(Number("  10  "));    // 10
    console.log(Number(""));          // 0
    console.log(Number(null));        // 0
    console.log(Number(true));        // 1
    console.log(Number(false));       // 0
    console.log(Number("hello"));     // NaN
    console.log(Number(undefined));   // NaN
    
  • 作为构造函数 (不推荐): 当使用 new Number() 创建时,它会返回一个Number 对象,而不是一个原始数字值。

    let numObj = new Number(10);
    console.log(typeof numObj);    // "object"
    console.log(numObj);           // [Number: 10]
    console.log(numObj === 10);    // false (对象与原始值不相等)
    console.log(numObj == 10);     // true (双等号会进行类型转换)
    

    为什么不推荐 new Number() 它创建的是一个对象,这可能导致一些意想不到的行为,尤其是在比较和类型检查时。在绝大多数情况下,我们只需要原始数字值。

3. 特殊的数字值

JavaScript 有三个特殊的数字值,它们在处理某些运算结果或边界条件时出现:

3.1 Infinity-Infinity
  • 表示: 代表比任何有限数值都大(或小)的数字。
  • 产生原因: 通常由超出数字表示范围的计算或除以零操作产生。
    console.log(1 / 0);          // Infinity
    console.log(-1 / 0);         // -Infinity
    console.log(Number.MAX_VALUE * 2); // Infinity
    
  • 特性:
    • Infinity 大于任何有限数,-Infinity 小于任何有限数。
    • Infinity + 1 仍然是 Infinity
    • Infinity / InfinityNaN
3.2 NaN (Not-a-Number)
  • 表示: 意为“非数字”,表示一个无效或无法表示的数值结果。
  • 产生原因: 通常由无效的数学运算(如 0/0Infinity - Infinity)、无法解析为数字的字符串转换(如 Number("hello"))等引起。
    console.log(0 / 0);           // NaN
    console.log(Infinity - Infinity); // NaN
    console.log("hello" * 2);     // NaN
    console.log(Math.sqrt(-1));   // NaN
    
  • 特性:
    • NaN 是 JavaScript 中唯一一个不等于自身的值。NaN === NaN 结果为 false
    • 任何涉及 NaN 的数学运算结果都是 NaN
  • 如何检查 NaN
    • 全局 isNaN() 函数: 存在一个缺陷,它会尝试将参数转换为数字。如果转换失败,即使参数本身不是 NaN,也会返回 true
      console.log(isNaN(NaN));      // true
      console.log(isNaN("hello"));  // true (因为 Number("hello") 是 NaN)
      console.log(isNaN("123"));    // false
      
    • Number.isNaN() 方法 (推荐): ES6 引入,更可靠,它只在参数严格等于 NaN 时才返回 true,不会进行隐式类型转换。
      console.log(Number.isNaN(NaN));      // true
      console.log(Number.isNaN("hello"));  // false (因为 "hello" 不是严格的 NaN)
      console.log(Number.isNaN("123"));    // false
      

4. Number 对象及其静态属性和方法

Number 对象既可以作为构造函数(不推荐),也可以作为静态对象,提供了一系列有用的常量和工具方法。

4.1 Number 对象的静态属性 (常量)
  • Number.MAX_VALUE: 最大可表示的有限数。
  • Number.MIN_VALUE: 最小可表示的正数(接近于零)。
  • Number.MAX_SAFE_INTEGER: 最大安全整数(2^53 - 1)。
  • Number.MIN_SAFE_INTEGER: 最小安全整数(-2^53 + 1)。
  • Number.EPSILON: 两个可表示数字之间最小的差值。用于浮点数比较。
  • Number.POSITIVE_INFINITY: 等同于全局的 Infinity
  • Number.NEGATIVE_INFINITY: 等同于全局的 -Infinity
  • Number.NaN: 等同于全局的 NaN
4.2 Number 对象的静态方法 (工具函数)
  • Number.isFinite(value): 检查一个值是否是有限的数字。它不会进行类型转换。
    console.log(Number.isFinite(123));     // true
    console.log(Number.isFinite(Infinity)); // false
    console.log(Number.isFinite(NaN));     // false
    console.log(Number.isFinite("123"));   // false (不会转换 "123")
    
  • Number.isInteger(value): 检查一个值是否是整数。
    console.log(Number.isInteger(10));     // true
    console.log(Number.isInteger(10.0));   // true
    console.log(Number.isInteger(10.5));   // false
    console.log(Number.isInteger(NaN));    // false
    console.log(Number.isInteger(Infinity)); // false
    console.log(Number.isInteger("10"));   // false
    
  • Number.isSafeInteger(value): 检查一个值是否是安全整数(即在 MIN_SAFE_INTEGERMAX_SAFE_INTEGER 之间)。
    console.log(Number.isSafeInteger(10));           // true
    console.log(Number.isSafeInteger(9007199254740991)); // true
    console.log(Number.isSafeInteger(9007199254740992)); // false (超出安全范围)
    
  • Number.isNaN(value): 推荐的 NaN 检查方法 (见上文)。
4.3 字符串解析方法 (Global 或 Number 静态方法)
  • parseInt(string, [radix])Number.parseInt(string, [radix]):
    • 解析字符串并返回一个整数。
    • 从字符串的开头开始解析,直到遇到非数字字符为止。
    • 第二个参数 radix 指定基数(2 到 36),非常重要,建议总是提供。
    console.log(parseInt("100"));       // 100 (默认基数10)
    console.log(parseInt("100px"));     // 100
    console.log(parseInt("  10.5  "));  // 10
    console.log(parseInt("0xF"));       // 15 (自动识别十六进制)
    console.log(parseInt("0F", 16));    // 15 (明确指定十六进制)
    console.log(parseInt("11", 2));     // 3 (二进制)
    console.log(parseInt("hello"));     // NaN
    
  • parseFloat(string)Number.parseFloat(string):
    • 解析字符串并返回一个浮点数。
    • parseInt 类似,但可以解析小数点。
    console.log(parseFloat("10.5"));    // 10.5
    console.log(parseFloat("3.14px"));  // 3.14
    console.log(parseFloat("  -0.25  ")); // -0.25
    console.log(parseFloat("hello"));   // NaN
    
4.4 Number.prototype 方法 (实例方法)

这些方法在数字值上调用,返回其字符串表示形式。

  • number.toFixed(digits):
    • 将数字格式化为指定小数位数的字符串,会进行四舍五入。
    • digits 参数是 0 到 100 之间的整数。
    let num = 3.14159;
    console.log(num.toFixed(2)); // "3.14"
    console.log(num.toFixed(0)); // "3"
    console.log((12.345).toFixed(2)); // "12.35"
    
  • number.toPrecision(precision):
    • 将数字格式化为指定总位数的字符串,会进行四舍五入。
    • precision 参数是 1 到 100 之间的整数。
    let num = 123.456;
    console.log(num.toPrecision(4)); // "123.5"
    console.log(num.toPrecision(2)); // "1.2e+2" (科学计数法)
    
  • number.toExponential(fractionDigits):
    • 将数字格式化为指数(科学计数)表示的字符串。
    • fractionDigits 指定小数部分位数。
    let num = 12345;
    console.log(num.toExponential(2)); // "1.23e+4"
    
  • number.toString([radix]):
    • 将数字转换为字符串。
    • radix 参数可选,指定转换的基数(2 到 36)。
    let num = 255;
    console.log(num.toString());    // "255" (默认基数10)
    console.log(num.toString(16));  // "ff" (十六进制)
    console.log(num.toString(2));   // "11111111" (二进制)
    
  • number.toLocaleString([locales], [options]):
    • 根据语言环境规则格式化数字。
    let cash = 123456.789;
    console.log(cash.toLocaleString('en-US'));  // "123,456.789"
    console.log(cash.toLocaleString('de-DE'));  // "123.456,789"
    console.log(cash.toLocaleString('en-US', { style: 'currency', currency: 'USD' })); // "$123,456.79"
    

5. 数学运算与 Math 对象

JavaScript 提供标准的算术运算符以及一个内置的 Math 对象,用于执行更复杂的数学任务。

5.1 算术运算符
  • + (加)
  • - (减)
  • * (乘)
  • / (除)
  • % (取模,余数)
  • ** (幂,ES6)
5.2 Math 对象

Math 对象不是构造函数,它所有的属性和方法都是静态的。

  • 常量:
    • Math.PI: 圆周率 (3.141592653589793)。
    • Math.E: 自然对数的底数 (e)。
  • 舍入方法:
    • Math.round(x): 四舍五入到最接近的整数。
    • Math.floor(x): 向下取整 (不大于 x 的最大整数)。
    • Math.ceil(x): 向上取整 (不小于 x 的最小整数)。
    • Math.trunc(x): 移除小数部分 (ES6),不进行四舍五入。
  • 其他常用方法:
    • Math.random(): 返回一个 0(包含)到 1(不包含)之间的浮点伪随机数。
    • Math.max(x1, x2, ...): 返回最大值。
    • Math.min(x1, x2, ...): 返回最小值。
    • Math.abs(x): 返回绝对值。
    • Math.pow(base, exponent): 返回基数的指数次幂。
    • Math.sqrt(x): 返回平方根。
    • Math.log(x): 返回自然对数。
    • Math.sin(x), Math.cos(x), Math.tan(x): 返回三角函数值 (参数为弧度)。

6. 浮点数精度问题 (Floating-Point Precision Issues)

这是 JavaScript 数字最常遇到的问题之一,由于其底层是二进制浮点数表示,某些十进制小数无法精确表示,导致计算结果不符合预期。

经典示例:

console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false

原因: 0.10.2 在二进制浮点数表示中都是无限循环小数,计算机只能截断存储,导致微小的误差。这些误差在计算时累积,最终导致结果不精确。

解决方案:

  1. 放大再缩小 (Multiply and Divide):
    将浮点数转换为整数进行计算,然后将结果缩小回来。

    function sum(a, b) {
      return (a * 10 + b * 10) / 10; // 适用于一位小数
    }
    console.log(sum(0.1, 0.2)); // 0.3
    

    对于更多小数位,需要计算最大公约数或确定最大小数位数。

    function addFloats(num1, num2) {
      let r1, r2, m;
      try { r1 = num1.toString().split(".")[1].length; } catch (e) { r1 = 0; }
      try { r2 = num2.toString().split(".")[1].length; } catch (e) { r2 = 0; }
      m = Math.pow(10, Math.max(r1, r2));
      return (Math.round(num1 * m) + Math.round(num2 * m)) / m;
    }
    console.log(addFloats(0.1, 0.2)); // 0.3
    console.log(addFloats(0.0001, 0.0007)); // 0.0008
    
  2. 使用 toFixed() (字符串化,不适用于后续计算):
    如果只是为了显示结果,toFixed() 可以提供指定精度的小数,但它返回的是字符串。

    console.log((0.1 + 0.2).toFixed(1)); // "0.3"
    

    如果需要转换为数字进行后续计算,需要再次 parseFloat(),但这并不能从根本上解决误差。

  3. 使用第三方库:
    对于金融、科学计算等需要高精度计算的场景,推荐使用专门的数学库,如 Decimal.jsBig.jsMath.js。这些库通常会使用字符串或其他内部表示来处理数字,避免浮点数精度问题。

7. BigInt:处理任意精度的整数 (ES2020)

BigInt 是 JavaScript 中一种新的原始数据类型,它允许你表示任意大的整数。这解决了 Number.MAX_SAFE_INTEGER 的限制。

  • 创建: 在整数后面加上 n 后缀,或使用 BigInt() 函数。
    const bigNum = 9007199254740991n; // 9007199254740991
    const anotherBigNum = bigNum + 2n; // 9007199254740993n (精确)
    const bigFromNumber = BigInt(123); // 123n
    
  • 运算: BigInt 只能与 BigInt 进行运算。不能将 BigIntNumber 混合使用。
    // console.log(10n + 5); // 错误: Cannot mix BigInt and other types
    console.log(10n + BigInt(5)); // 15n
    
  • 除法: BigInt 除法会舍弃小数部分,因为它只处理整数。
    console.log(10n / 3n); // 3n
    
  • 比较: 可以和 Number 进行比较。
    console.log(10n > 5);   // true
    console.log(10n === 10); // false (类型不同)
    

BigInt 主要用于处理大整数,不能用于表示小数。

8. 最佳实践与常见陷阱

  • 始终使用数字字面量: 避免使用 new Number() 构造函数创建数字对象。
  • 优先使用 Number.isNaN()Number.isFinite() 这些方法比全局的 isNaN()isFinite() 更可靠,因为它们不会进行隐式类型转换。
  • 注意 parseInt() 的基数: 总是为 parseInt() 提供 radix 参数(如 parseInt(str, 10)),以避免意外行为。
  • 警惕浮点数精度问题: 对于需要精确计算的场景,尤其是在货币处理时,要特别小心。考虑使用放大缩小法或高精度数学库。
  • 了解安全整数范围: 当处理超过 Number.MAX_SAFE_INTEGER 的整数时,考虑使用 BigInt
  • 避免在数字和 BigInt 之间隐式转换: 它们是两种不同的类型,直接混合运算会报错。

9. 总结与展望

JavaScript 的数字类型虽然单一,但其基于 IEEE 754 标准的设计带来了独特的行为和强大的功能。

  • 核心: 所有数字都是 64 位浮点数,包括整数。
  • 特殊值: Infinity-InfinityNaN 处理边界情况和无效运算。
  • Number 对象: 提供实用常量 (MAX_SAFE_INTEGER, EPSILON) 和方法 (isInteger, parseInt, toFixed)。
  • 精度问题: 浮点数运算存在精度误差,需要特定方法或库来解决。
  • BigInt 专门用于表示和操作任意大的整数,解决了 Number 类型的整数精度限制。

通过深入理解 JavaScript 数字的这些特性,你将能够更自信、更精确地处理各种数值计算,编写出更健壮、更可靠的 JavaScript 应用程序。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐