Math 对象

Math 对象集中提供取整、幂、根、三角函数、极值与伪随机数;理解浮点边界和区间契约,才能避免精度与安全错误。

难度 入门 时长 标准深度约 14分钟
版本 Node 24
what

Math 是不可构造的内置对象,以静态属性和方法提供常见数学运算。它处理的是 JavaScript Number,不是任意精度十进制数或 BigInt

trap

取整方向、浮点误差、NaN 传播和随机区间端点都容易被看似合理的公式掩盖。Math.random() 也不适合令牌、验证码或其他安全用途。

fix

先写清数值域、区间和误差预算,再选择方法;验证边界输入,并把安全随机数交给 Web Crypto。

是什么,为什么存在

Math 是 JavaScript 的内置对象,把圆周率等常量和常见数值算法放在一个命名空间中。你直接调用 Math.sqrt(9) 或读取 Math.PI,不需要也不能用 new Math() 创建实例。它不是函数,也没有供实例继承的方法集合。

这个对象解决的是通用 Number 运算,而不是所有数学问题。取绝对值、求极值、取整、计算幂和根、处理三角函数以及生成普通伪随机数时,通常会遇到它。加减乘除和取余仍由语言运算符完成。

Math 方法接受并返回 JavaScript 数值。多数业务代码的真正难点不在方法名称,而在输入契约:值是否有限、是否允许负数、端点是否包含、结果是否需要十进制精确,以及随机性是否关系到安全。

Math 不提供向量、矩阵、复数、统计分布或任意精度十进制类型。需求超出标量运算时,应先定义数据模型和精度要求,再选择专门实现;不要把不断增长的辅助函数误称为 Math 本身的能力。

工作原理

Math 的属性都是静态成员。常量包括 Math.PIMath.E,方法则按用途分成几组:取整、符号与绝对值,幂、根与对数,三角函数,极值与距离,以及随机数。

用途常见成员关键契约
取整floorceiltruncround方向不同,负数最能暴露差异
幂与根sqrtcbrtpowexplog域外输入通常产生 NaN 或无穷值
三角函数sincostanatan2角度使用弧度,不使用度数
聚合minmaxhypotminmax 接收独立实参,不直接接收数组
随机数random返回 01 的半开区间值,不提供种子参数

取整方向

四个常用取整方法不是同义词。Math.floor(x) 朝负无穷取整,Math.ceil(x) 朝正无穷取整,Math.trunc(x) 朝零截断。Math.round(x) 选择最近整数;恰好位于两个整数中间时,结果朝正无穷方向,因此 Math.round(-2.5)-2,不是银行家舍入的 -2-3 交替规则。

输入floorceiltruncround
2.72323
-2.7-3-2-2-3
-2.5-3-2-2-2

Math.round(-0.5) 还会产生负零。负零大多数时候显示为 0,但 Object.is(value, -0) 能区分它,1 / -0 也会得到 -Infinity。当零的方向携带计算意义时,不要只看格式化输出。

Math.fround() 名称中虽然有「round」,但它把值舍入为最接近的 32 位浮点表示,不是按十进制位数四舍五入。Math.clz32() 等 32 位方法也属于位级数值工具,不应替代一般整数验证。

参数转换与特殊值

多数 Math 方法会把参数转换为 Number,所以 Math.abs('-3') 返回 3,而空字符串可能变成 0。这种便利不等于可靠的输入验证。API 边界应先拒绝不允许的类型,再用 Number.isFinite()Number.isInteger()Number.isSafeInteger() 检查所需数值域。

BigInt 不能传给要求 NumberMath 方法。为了让调用通过而机械使用 Number(bigint),可能先丢失整数精度。只需要整数运算时保留 BigInt 运算符;确实要转换时,先证明值落在安全范围内。

特殊值会沿计算传播。Math.sqrt(-1)NaNMath.log(0)-Infinity,任何一个 NaN 参数都会让 Math.max()Math.min() 返回 NaN。这些返回值通常不会立即抛错,因此应在产生处或系统边界明确验证。

空参数也有定义:Math.max() 返回 -InfinityMath.min() 返回 Infinity。它们是聚合运算的恒等边界,不一定符合「没有数据」的业务语义。空集合应该返回 null、抛错还是使用默认值,需要由调用契约决定。

浮点结果

JavaScript Number 使用 二进制浮点数(binary floating-point) 。许多十进制小数不能被有限二进制位精确表示,所以 0.1 + 0.2 与源码中的 0.3 不是同一个浮点值。三角函数、对数和根也可能留下末位误差。

Number.EPSILON1 与下一个更大可表示数之间的差,不是适用于所有量级的全局容差。比较近似结果时,容差必须来自业务误差预算,并根据数值尺度决定是否结合绝对误差与相对误差。近零比较尤其需要绝对容差。

规范允许部分超越函数使用实现近似值。不同合规引擎可能在最后几位上不同,因此跨运行时测试不应断言无意义的完整小数展开。若业务要求可移植的精确十进制结果,Math 和二进制 Number 不是完整方案。

随机数区间

Math.random() 返回大于等于 0 且小于 1 的近似均匀伪随机 Number。这个 半开区间(half-open interval) 记为 [0, 1);左端可出现,右端不会出现。整数映射公式必须同时写清目标区间是否包含上界。

JavaScript API 不允许给 Math.random() 设置种子,算法也由实现选择。因此,它适合界面抖动、普通抽样和非安全游戏逻辑,却不适合可复现实验或安全凭据。测试需要固定序列时,应把随机源作为依赖传入。

安全令牌、验证码和不可预测标识需要 密码学安全随机性(cryptographically secure randomness) 。浏览器与 Node 24 都提供 Web Crypto 的 crypto.getRandomValues();把随机字节均匀映射到任意区间仍要处理取模偏差,因此优先使用经过审查的高层接口或拒绝采样。

先确定数值契约

选择方法前,先把自然语言需求改写成可测试的数值契约。「限制百分比」至少要说明输入是否允许字符串、有效区间是 [0, 100] 还是 [0, 1],以及 NaN 应抛错还是传播。「随机选一个索引」则要说明空数组和上界是否可能被选中。

一份最小契约通常覆盖以下项目:

  1. 输入是 NumberBigInt 还是允许转换的文本。
  2. 单位是弧度、度数、像素、秒还是最小货币单位。
  3. 区间的每个端点是否包含,以及空区间是否合法。
  4. 允许绝对误差、相对误差,还是要求十进制精确。
  5. NaN、无穷值、负零和空集合如何处理。
  6. 随机结果只需普通分布、必须可复现,还是必须抵抗预测。

方法名不能替代这些决定。Math.max() 不知道空数组在业务上表示「没有观测」,Math.round() 不知道发票采用哪种中点规则,Math.random() 也不知道生成结果是否会成为身份凭据。

验证应靠近系统边界和语义转换点。若函数把度数转换为弧度,就在转换前验证度数范围,并用新变量名保存弧度;若函数把测量值限制在范围内,则先决定异常输入应被拒绝还是限制,避免 NaN 被伪装成正常边界值。

示例

下面四个示例依次展示取整、坐标计算、近似比较和可测试的随机整数。输出均来自本地 Node 24 实际执行对应文件。

对比取整方向

先用正数、负数和中点值观察差异。Object.is() 单独检查负零,避免控制台把它显示成普通零。

rounding.js
const values = [2.7, -2.7, -2.5];

for (const value of values) {
  console.log(
    `${value}: floor=${Math.floor(value)}, ceil=${Math.ceil(value)}, ` +
      `trunc=${Math.trunc(value)}, round=${Math.round(value)}`,
  );
}

console.log('round(-0.5) is negative zero:', Object.is(Math.round(-0.5), -0));
console.log('hypot(3, 4):', Math.hypot(3, 4));
console.log('max(7, 12, 4):', Math.max(7, 12, 4));
2.7: floor=2, ceil=3, trunc=2, round=3
-2.7: floor=-3, ceil=-2, trunc=-2, round=-3
-2.5: floor=-3, ceil=-2, trunc=-2, round=-2
round(-0.5) is negative zero: true
hypot(3, 4): 5
max(7, 12, 4): 12

Math.hypot(3, 4) 直接计算欧几里得长度,并得到 5Math.max() 接收的是三个独立实参。示例把不同类别放在一起,是为了显示它们都返回普通 Number,而不是创建数学对象。

限制坐标并计算方向

这个辅助函数先验证有限值和上下界,再用 minmax 组合实现范围限制。Math.atan2(y, x) 返回弧度,展示为度数时才执行换算。

marker-position.js
function clamp(value, minimum, maximum) {
  if (![value, minimum, maximum].every(Number.isFinite) || minimum > maximum) {
    throw new RangeError('expected finite values and minimum <= maximum');
  }
  return Math.max(minimum, Math.min(maximum, value));
}

function placeMarker(point, viewport) {
  const x = clamp(point.x, 0, viewport.width);
  const y = clamp(point.y, 0, viewport.height);
  const angle = Math.atan2(point.y, point.x);

  return {
    x,
    y,
    distanceFromOrigin: Math.hypot(point.x, point.y),
    angleDegrees: angle * 180 / Math.PI,
  };
}

console.log(placeMarker({ x: 300, y: 400 }, { width: 280, height: 450 }));
{
  x: 280,
  y: 400,
  distanceFromOrigin: 500,
  angleDegrees: 53.13010235415598
}

原始点超出视口宽度,所以返回的 x 被限制为 280。距离和方向仍基于原始点计算,这是函数契约的一部分;若应基于限制后坐标计算,就必须把 xy 传给 hypotatan2

按误差预算比较

近似比较先处理完全相等与非有限值,再把绝对容差和随尺度增长的相对容差结合起来。默认值只是示例策略,真实系统应按测量精度、算法误差和单位确定阈值。

nearly-equal.js
function nearlyEqual(
  left,
  right,
  { relativeTolerance = 1e-12, absoluteTolerance = Number.EPSILON } = {},
) {
  if (!Number.isFinite(left) || !Number.isFinite(right)) return left === right;
  if (left === right) return true;

  const difference = Math.abs(left - right);
  const scale = Math.max(Math.abs(left), Math.abs(right));
  return difference <= Math.max(absoluteTolerance, relativeTolerance * scale);
}

console.log('0.1 + 0.2 === 0.3:', 0.1 + 0.2 === 0.3);
console.log('nearly equal:', nearlyEqual(0.1 + 0.2, 0.3));
console.log('large values:', nearlyEqual(1_000_000_000_000, 1_000_000_000_000.5));
console.log(
  'near zero:',
  nearlyEqual(1e-15, 0, { absoluteTolerance: 1e-14 }),
);
0.1 + 0.2 === 0.3: false
nearly equal: true
large values: true
near zero: true

有限值之外,这个函数只把严格相等的值视为相等,所以两个 Infinity 相等,而 NaN 不等于自身。是否接受无穷值也是契约选择;测量数据通常应在调用比较函数前拒绝它们。

注入随机源

生成闭区间整数时,跨度是 maximum - minimum + 1。示例验证安全整数与范围顺序,并注入固定随机序列,使边界行为可以重复测试。

random-integer.js
function randomIntInclusive(minimum, maximum, random = Math.random) {
  if (!Number.isSafeInteger(minimum) || !Number.isSafeInteger(maximum)) {
    throw new TypeError('bounds must be safe integers');
  }
  if (minimum > maximum) {
    throw new RangeError('minimum must not exceed maximum');
  }

  const span = maximum - minimum + 1;
  if (!Number.isSafeInteger(span)) {
    throw new RangeError('range is too wide');
  }
  return minimum + Math.floor(random() * span);
}

const samples = [0, 0.49, 0.999999];
let index = 0;
const replay = () => samples[index++];

console.log(randomIntInclusive(1, 6, replay));
console.log(randomIntInclusive(1, 6, replay));
console.log(randomIntInclusive(1, 6, replay));
1
3
6

注入只解决可测试性,不会让 Math.random() 变成安全随机源,也不会保证极宽区间完全无偏。生产调用若使用默认参数,仍继承 Math.random() 的全部限制;安全用途应切换到专门的密码学接口。

陷阱

Math.round() 当成十进制或财务舍入

修复方法: 先定义舍入模式和数据表示。金额可在范围允许时使用最小货币单位的安全整数;需要严格十进制语义时使用经过验证的十进制实现。toFixed(2) 适合生成展示字符串,但不要把格式化误当成存储精度。

用一个固定 Number.EPSILON 比较所有结果

修复方法: 从领域误差预算推导绝对与相对容差,并为零、极大值和阈值两侧写测试。对必须完全相等的离散计数仍使用严格比较,不要无条件改成近似比较。

混淆负数取整和中点规则

修复方法: 用方向名称描述需求,再选 floorceiltrunc 或明确的舍入算法。测试正负小数、中点、负零、超出 32 位范围的值以及 NaN,不要以微基准传言为理由换成位运算。

直接展开大型数组求极值

修复方法: 小型且已验证的集合可以展开。规模不受控时用循环或带明确初值的 reduce 逐项聚合,并在聚合前决定空集合和无效数值的处理方式。

Math.random() 用于安全或可复现结果

修复方法: 安全用途使用 Web Crypto 或平台提供的安全高层 API,并检查区间映射是否有偏。模拟和测试把明确的伪随机生成器作为依赖传入,记录种子,但不要全局替换 Math.random() 影响无关代码。

忽略单位、数值域和 NaN

修复方法: 在名称中标出 angleRadiansangleDegrees,并在边界验证有限值与允许范围。每个可能生成 NaN 或无穷值的步骤都要决定是拒绝、钳制还是显式传播。

深入 浮点边界与误差预算

浮点边界与误差预算

二进制浮点把一个有限数编码为符号、有效数字和二进制指数。JavaScript Number 对应 IEEE 754 双精度格式,但这不意味着每个十进制数都精确。整数只在安全整数范围内保证逐个可表示;超过 Number.MAX_SAFE_INTEGER 后,相邻数学整数可能映射到同一个 Number

误差来源至少要分成表示误差、算法近似和输入噪声。0.1 的表示误差来自十进制到二进制转换,Math.sin() 还涉及函数近似,而传感器读数本身可能已经有测量误差。把三者都用一个随手选择的 1e-10 覆盖,会让比较既无法解释,也难以维护。

相对容差随数值尺度增长,适合比较远离零且量级不同的结果;绝对容差在零附近提供固定误差带。常见策略是接受 |a - b| <= max(absTol, relTol * max(|a|, |b|)),但容差值仍必须来自领域。临界业务判断还要明确等号落在哪一侧。

舍入发生的阶段同样重要。每一步都舍入会累积偏差,只在最终展示时舍入又可能让中间值超出业务允许精度。财务、计费和法规计算应由领域规则规定数据表示、每次舍入的位置以及中点模式,而不是由通用 Math.round() 猜测。

负零与非有限值

负零保留了趋近方向或符号操作的结果。Math.sign(-0) 仍是 -0Math.min(0, -0) 选择 -0,但字符串格式通常隐藏符号。只有当方向影响倒数、坐标变换或协议时才需要保留它,否则可在系统边界规范化为普通零。

NaN 表示数值运算没有得到可用数值,却不说明失败原因。Infinity 可能来自除零、溢出或有定义的函数边界。若错误原因需要诊断,应该在执行 Math 调用前验证并抛出带上下文的错误,而不是等到最后只看到 NaN

聚合与数值稳定性

Math.hypot(...values) 在计算平方和的平方根时会缩放中间值,能避免手写 Math.sqrt(x * x + y * y) 更容易遇到的过早上溢或下溢。它仍返回 Number,也不会替调用方验证单位一致性。经纬度、像素和米不能因为都能传入同一方法就直接混合。

Math.max()Math.min() 的空参数结果来自它们的数学恒等边界。业务聚合常需要不同语义,例如空数据返回 null,或把缺失视为错误。先定义空集合结果,再选择初值,可以避免一个合法的 Infinity 悄悄进入后续 JSON 或数据库流程。

对大型集合逐项循环不仅绕开调用参数上限,也能在同一位置验证值、记录无效项并实现早停。是否需要补偿求和或其他稳定算法取决于误差预算与数据分布;没有测量和需求时,不要声称某个微优化必然更快或更准。

幂、对数与角度的边界

Math.sqrt(x) 只返回主平方根,负有限实数没有实数平方根,所以结果是 NaNMath.cbrt(x) 可以处理负数,因为负数存在实数立方根。调用方若在复数域工作,需要不同的数据类型和算法。

Math.pow(base, exponent)base ** exponent 对普通数值表达相同的幂运算,但运算符语法有自己的优先级限制。尤其不能直接写 -2 ** 2;若底数是负数,应写 (-2) ** 2,若要对幂结果取负,则写 -(2 ** 2)

Math.log(x) 计算自然对数,不是以 10 为底。常用底数有独立的 Math.log10()Math.log2()。对数的定义域也应显式检查:负输入得到 NaN,零得到 -Infinity,这两种情况通常代表不同的输入错误。

三角函数统一接收弧度,而 Math.atan2(y, x) 还规定了实参顺序。交换 xy 会得到一个仍然合理但方向错误的角度。为转换系数命名,并用轴线、象限和零向量测试,比只测试一个四十五度角更容易发现错误。

反三角函数的输入也有定义域。浮点计算得到的余弦值可能略微越过 [-1, 1],几何代码在已经验证两个向量非零后,可以根据算法证明把值限制回该区间。不要对任意无效输入都先钳制,因为那会隐藏真正的数据错误。

表达式结果需要确认的契约
Math.sqrt(-1)NaN是否只允许实数域
Math.log(0)-Infinity零是边界值还是无效输入
Math.acos(1.0000000000000002)NaN越界来自误差还是坏数据
Math.atan2(0, 0)0零向量是否有定义方向

表中的结果都是语言行为,不自动等于业务答案。例如零向量没有自然方向,即使 Math.atan2(0, 0) 返回可用的 Number。调用方仍要在进入通用数学函数前落实领域不变量。

随机性的三份契约

普通界面随机、可复现实验和安全随机是三种不同契约。Math.random() 只覆盖第一种。可复现实验需要明确算法与种子,安全随机需要能抵抗预测的熵源和经过审查的派生方法。

仅记录种子仍不足以长期复现结果;伪随机算法或采样步骤改变后,同一种子可能产生不同序列。需要重放的模拟还应记录算法标识、版本、种子和输入顺序。

[0, 1) 映射为 [min, max] 的常用乘法公式适合跨度不大的普通抽样。浮点随机源只有有限状态和有限可表示输出,目标跨度很大时不能假设每个整数机会完全相同。若公平性可以被审计,应指定算法、输入熵、区间映射和记录方式。

对随机无符号整数直接执行 % span 也可能产生取模偏差,因为源空间大小未必是 span 的整数倍。拒绝采样会丢弃不能均匀分组的尾部区域,再对接受值取模。这个实现细节容易出错,安全代码应优先调用能直接生成目标范围的可信平台接口。

测试随机逻辑时,注入最小值附近、中间值和接近上界的确定序列。统计测试可以发现明显偏差,却不能证明密码学安全。安全结论来自所用原语、熵源、威胁模型和实现审查,不来自一张看起来均匀的直方图。

延伸阅读

检查点

4个问题 · 1 道输出预测题 · 1 道找错题

下一篇 数组方法 Web crypto 即将上线 Date 对象
复制为 Markdown 面试题库 在 GitHub 上编辑 报告错误 讲清楚了吗?