Math 是不可构造的内置对象,以静态属性和方法提供常见数学运算。它处理的是 JavaScript Number,不是任意精度十进制数或 BigInt。
取整方向、浮点误差、NaN 传播和随机区间端点都容易被看似合理的公式掩盖。Math.random() 也不适合令牌、验证码或其他安全用途。
先写清数值域、区间和误差预算,再选择方法;验证边界输入,并把安全随机数交给 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 应抛错还是传播。「随机选一个索引」则要说明空数组和上界是否可能被选中。
一份最小契约通常覆盖以下项目:
- 输入是
Number、BigInt还是允许转换的文本。 - 单位是弧度、度数、像素、秒还是最小货币单位。
- 区间的每个端点是否包含,以及空区间是否合法。
- 允许绝对误差、相对误差,还是要求十进制精确。
NaN、无穷值、负零和空集合如何处理。- 随机结果只需普通分布、必须可复现,还是必须抵抗预测。
方法名不能替代这些决定。Math.max() 不知道空数组在业务上表示「没有观测」,Math.round() 不知道发票采用哪种中点规则,Math.random() 也不知道生成结果是否会成为身份凭据。
验证应靠近系统边界和语义转换点。若函数把度数转换为弧度,就在转换前验证度数范围,并用新变量名保存弧度;若函数把测量值限制在范围内,则先决定异常输入应被拒绝还是限制,避免 NaN 被伪装成正常边界值。
示例
下面四个示例依次展示取整、坐标计算、近似比较和可测试的随机整数。输出均来自本地 Node 24 实际执行对应文件。
对比取整方向
先用正数、负数和中点值观察差异。Object.is() 单独检查负零,避免控制台把它显示成普通零。
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): 12Math.hypot(3, 4) 直接计算欧几里得长度,并得到 5。Math.max() 接收的是三个独立实参。示例把不同类别放在一起,是为了显示它们都返回普通 Number,而不是创建数学对象。
限制坐标并计算方向
这个辅助函数先验证有限值和上下界,再用 min 与 max 组合实现范围限制。Math.atan2(y, x) 返回弧度,展示为度数时才执行换算。
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。距离和方向仍基于原始点计算,这是函数契约的一部分;若应基于限制后坐标计算,就必须把 x 与 y 传给 hypot 和 atan2。
按误差预算比较
近似比较先处理完全相等与非有限值,再把绝对容差和随尺度增长的相对容差结合起来。默认值只是示例策略,真实系统应按测量精度、算法误差和单位确定阈值。
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。示例验证安全整数与范围顺序,并注入固定随机序列,使边界行为可以重复测试。
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 比较所有结果
修复方法: 从领域误差预算推导绝对与相对容差,并为零、极大值和阈值两侧写测试。对必须完全相等的离散计数仍使用严格比较,不要无条件改成近似比较。
混淆负数取整和中点规则
修复方法: 用方向名称描述需求,再选 floor、ceil、trunc 或明确的舍入算法。测试正负小数、中点、负零、超出 32 位范围的值以及 NaN,不要以微基准传言为理由换成位运算。
直接展开大型数组求极值
修复方法: 小型且已验证的集合可以展开。规模不受控时用循环或带明确初值的 reduce 逐项聚合,并在聚合前决定空集合和无效数值的处理方式。
把 Math.random() 用于安全或可复现结果
修复方法: 安全用途使用 Web Crypto 或平台提供的安全高层 API,并检查区间映射是否有偏。模拟和测试把明确的伪随机生成器作为依赖传入,记录种子,但不要全局替换 Math.random() 影响无关代码。
忽略单位、数值域和 NaN
修复方法: 在名称中标出 angleRadians 或 angleDegrees,并在边界验证有限值与允许范围。每个可能生成 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) 仍是 -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 的整数倍。拒绝采样会丢弃不能均匀分组的尾部区域,再对接受值取模。这个实现细节容易出错,安全代码应优先调用能直接生成目标范围的可信平台接口。
测试随机逻辑时,注入最小值附近、中间值和接近上界的确定序列。统计测试可以发现明显偏差,却不能证明密码学安全。安全结论来自所用原语、熵源、威胁模型和实现审查,不来自一张看起来均匀的直方图。
延伸阅读
4个问题 · 1 道输出预测题 · 1 道找错题