# Math 对象

Source: https://codewiki.com/zh/javascript/math-object/

> - **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.PI` 和 `Math.E`，方法则按用途分成几组：取整、符号与绝对值，幂、根与对数，三角函数，极值与距离，以及随机数。

| 用途 | 常见成员 | 关键契约 |
| --- | --- | --- |
| 取整 | `floor`、`ceil`、`trunc`、`round` | 方向不同，负数最能暴露差异 |
| 幂与根 | `sqrt`、`cbrt`、`pow`、`exp`、`log` | 域外输入通常产生 `NaN` 或无穷值 |
| 三角函数 | `sin`、`cos`、`tan`、`atan2` | 角度使用弧度，不使用度数 |
| 聚合 | `min`、`max`、`hypot` | `min` 和 `max` 接收独立实参，不直接接收数组 |
| 随机数 | `random` | 返回 `0` 到 `1` 的半开区间值，不提供种子参数 |

### 取整方向

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

| 输入 | `floor` | `ceil` | `trunc` | `round` |
| ---: | ---: | ---: | ---: | ---: |
| `2.7` | `2` | `3` | `2` | `3` |
| `-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` 不能传给要求 `Number` 的 `Math` 方法。为了让调用通过而机械使用 `Number(bigint)`，可能先丢失整数精度。只需要整数运算时保留 `BigInt` 运算符；确实要转换时，先证明值落在安全范围内。

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

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

### 浮点结果

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

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

规范允许部分超越函数使用实现近似值。不同合规引擎可能在最后几位上不同，因此跨运行时测试不应断言无意义的完整小数展开。若业务要求可移植的精确十进制结果，`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. 输入是 `Number`、`BigInt` 还是允许转换的文本。
2. 单位是弧度、度数、像素、秒还是最小货币单位。
3. 区间的每个端点是否包含，以及空区间是否合法。
4. 允许绝对误差、相对误差，还是要求十进制精确。
5. `NaN`、无穷值、负零和空集合如何处理。
6. 随机结果只需普通分布、必须可复现，还是必须抵抗预测。

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

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

## 示例

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

### 对比取整方向

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

<!-- quick -->

```javascript
// file: 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));
```

```text
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
```

<!-- /quick -->

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

### 限制坐标并计算方向

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

```javascript
// file: 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 }));
```

```text
{
  x: 280,
  y: 400,
  distanceFromOrigin: 500,
  angleDegrees: 53.13010235415598
}
```

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

### 按误差预算比较

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

```javascript
// file: 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 }),
);
```

```text
0.1 + 0.2 === 0.3: false
nearly equal: true
large values: true
near zero: true
```

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

### 注入随机源

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

```javascript
// file: 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));
```

```text
1
3
6
```

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

## 陷阱

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

> **陷阱:** `Math.round(value * 100) / 100` 会先执行二进制浮点乘法。`1.005 * 100` 的实际表示可能略小于预期中点，因此公式无法普遍实现精确的两位十进制舍入。

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

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

> **陷阱:** `Math.abs(a - b) < Number.EPSILON` 只在接近 `1` 的尺度上有特定含义。数值很大时它通常过严，数值接近零而业务噪声更大时也可能过严。

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

### 混淆负数取整和中点规则

> **陷阱:** `floor` 不是「去掉小数」，`round` 也不是银行家舍入。生成代码常用 `value | 0` 或 `~~value` 替代取整，却把值转换为有符号 32 位整数，可能产生回绕和错误的非有限值处理。

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

### 直接展开大型数组求极值

> **陷阱:** `Math.max(...values)` 对小数组清楚，但展开会把每个元素变成函数实参。数组足够大时可能超过引擎调用参数上限；空数组和含 `NaN` 的数组也会得到容易漏检的结果。

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

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

> **陷阱:** `Math.random()` 没有安全强度保证，也没有标准种子接口。验证码、重置令牌、会话标识和抽签审计若依赖它，会得到错误的威胁模型；测试若直接调用它，又会产生不稳定结果。

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

### 忽略单位、数值域和 `NaN`

> **陷阱:** 三角函数把度数当作弧度时仍会返回普通数值，`Math.sqrt()` 收到负数时也只返回 `NaN`。这类错误能穿过多层计算，直到序列化、渲染或数据库边界才暴露。

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

<!-- deep -->

## 浮点边界与误差预算

二进制浮点把一个有限数编码为符号、有效数字和二进制指数。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)` 仍是 `-0`，`Math.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)` 只返回主平方根，负有限实数没有实数平方根，所以结果是 `NaN`。`Math.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)` 还规定了实参顺序。交换 `x` 与 `y` 会得到一个仍然合理但方向错误的角度。为转换系数命名，并用轴线、象限和零向量测试，比只测试一个四十五度角更容易发现错误。

反三角函数的输入也有定义域。浮点计算得到的余弦值可能略微越过 `[-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` 的整数倍。拒绝采样会丢弃不能均匀分组的尾部区域，再对接受值取模。这个实现细节容易出错，安全代码应优先调用能直接生成目标范围的可信平台接口。

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

<!-- /deep -->

[检查点: javascript/math-object](https://codewiki.com/zh/javascript/math-object/#checkpoint)

## 延伸阅读

- [ECMAScript 语言规范：Math 对象](https://tc39.es/ecma262/multipage/numbers-and-dates.html#sec-math-object)
- [MDN：Math](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math)
- [MDN：`Math.random()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random)
- [MDN：`Crypto.getRandomValues()`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)
- [MDN：`Number.EPSILON`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/EPSILON)
