Skip to content

数值:精度、校验、Math 与 BigInt ​

数值问题经常不是算式写错,而是表示范围、单位和输入含义没有对齐。Number 适合日常计算;BigInt 解决整数规模问题;两者都不会替你决定业务上的舍入规则。

Number 的精度为什么有限 ​

Number 使用 IEEE 754 双精度二进制浮点表示。有效精度有限,某些十进制小数无法精确表示,就像十进制写不完三分之一。

js
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740992 === 9007199254740993); // true

最后一行中右边的数在表示时已经舍入,比较无法恢复丢失的信息。安全整数范围是 -(2 ** 53 - 1) 到 2 ** 53 - 1;范围之外并非所有整数都不可表示,而是不能保证相邻整数都能区分。

Number.EPSILON 是 1 附近相邻可表示值的间隔,不是任何业务都能直接使用的误差阈值。数据规模较大时需要相对误差,接近零时需要绝对误差;阈值应由测量精度和用途决定。货币若最小单位固定为分,可用安全整数表示分,并在输入边界约定舍入,不要把 toFixed 当万能精确运算。

先用一个能精确表示的小数理解二进制 ​

十进制的1.25可以写成 1 + 1/4,二进制是 1.01,只需要有限位;0.1在二进制中却需要不断重复,有限的有效位必须舍入。浮点误差不是所有小数都错,更不是某条加法指令随机抽风。

js
console.log(0.5 + 0.25 === 0.75); // true
console.log(1.25 * 4 === 5); // true
console.log(0.1 + 0.2 === 0.3); // false

双精度数的52位小数部分加上正常数隐含的最高位,提供53位有效二进制精度。指数负责移动小数点;数的量级变大时,相邻可表示数之间的距离也变大。在 2 ** 53 附近,间隔已经是2,所以继续加1可能回到同一可表示值,加2才能前进。

js
const edge = 2 ** 53;
console.log(edge + 1 === edge); // true
console.log(edge + 2 === edge); // false

这解释了“范围”和“精度”不同:Number可以表示极大的量级,不等于能区分该量级附近每个整数。BigInt适合需要逐个整数精确区分的计算,而不是因为它听起来“大”就替换所有小数。

转换之后检查有限、整数与范围 ​

js
const text = '12';
const quantity = Number(text);
const valid = text.trim() !== ''
  && Number.isFinite(quantity)
  && Number.isInteger(quantity)
  && quantity >= 0
  && quantity <= 100;
console.log(valid); // true
console.log(Number.isNaN(NaN), Number.isNaN('x')); // true false
console.log(isNaN('x')); // true:全局版本先转换

这里 trim() 去掉文本两端空白,详细字符串方法在第三章。先拒绝空白是因为 Number('') 会得到 0。isFinite 的全局版本也会转换,校验已经转换的数值通常选择 Number.isFinite。

NaN 仍属于 number 类型;用 Number.isNaN 识别。除以零可能得到 Infinity,零除以零得到 NaN。Number.isInteger(Infinity) 为假,Number.isSafeInteger 还检查整数精度范围。Number.MIN_VALUE 是最小正非零数,不是最负的数;最负有限数是 -Number.MAX_VALUE。

Math 做计算,格式化方法产生字符串 ​

js
console.log(Math.floor(-1.2)); // -2,向负无穷
console.log(Math.ceil(-1.2));  // -1,向正无穷
console.log(Math.trunc(-1.2)); // -1,去掉小数部分
console.log(Math.round(-1.5)); // -1,半值朝正无穷
console.log((12.345).toFixed(2)); // '12.35':字符串
console.log((255).toString(16)); // 'ff'

Math.abs 取绝对值;min/max 求边界;sqrt/cbrt 求根;pow 或 ** 求幂;log/log2/log10 与 exp/expm1/log1p 处理指数对数;sin/cos/tan 与对应反函数使用弧度;hypot 计算平方和的平方根。选择函数时留意定义域,例如负数的实数平方根得到 NaN。

toPrecision 按有效数字格式化,toExponential 输出科学计数文本,toLocaleString 按地区展示。格式化结果用于阅读,不适合作为下一轮算术输入。

Math.random() 返回大于等于 0、小于 1 的伪随机数。Math.floor(Math.random() * 6) + 1 可生成普通骰子点数;它不适合令牌和密码。安全随机数属于宿主的加密 API,后续平台章节处理。

舍入是业务规则,格式化只是显示步骤 ​

js
const display = (2.675).toFixed(2);
console.log(display, typeof display); // 2.67 string
console.log(display + 1); // 2.671

2.675存入Number时已经是附近的二进制近似值,因此不能只按纸面十进制“第三位是5”预测显示结果。toFixed返回文本,下一次 + 1 又会变成拼接。这两种问题分别属于数值表示和返回类型,不能靠再多调用一次toFixed解决。

需要误差比较时,先规定输入为有限数值,再选择绝对与相对阈值。以下只展示表达式,不封装尚未学习的函数:

js
const actual = 10000.1 + 10000.2;
const expected = 20000.3;
const difference = Math.abs(actual - expected);
const absoluteTolerance = 1e-12;
const relativeTolerance = 1e-12;
const allowed = Math.max(
  absoluteTolerance,
  relativeTolerance * Math.max(Math.abs(actual), Math.abs(expected)),
);
console.log(difference <= allowed); // true

绝对阈值约束接近零的数据,相对阈值随数值尺度增长。这里的阈值只是示例,不能直接复制到金额、测量或统计场景并声称正确。金额以“分”存为安全整数可以避免许多加减误差,但折扣、税率、拆分仍可能产生不足一分的结果,需要规定在每项、每单还是最终汇总时舍入。

负零也是浮点表示的一部分:0 === -0为true,但 1 / -0为-Infinity,Object.is可以区分方向信息。格式化通常把它隐藏,不能把显示文本相同等同于底层比较规则完全相同。

位运算不是免费的大整数转换 ​

js
console.log(2 ** 32 | 0); // 0:Number 位运算会截到 32 位
console.log(5 & 3);       // 1,0101 与 0011
console.log(5 | 3);       // 7
console.log(5 ^ 3);       // 6
console.log(-1 >>> 0);    // 4294967295

Number 的大多数位运算使用有符号 32 位整数,>>> 返回无符号结果;移位次数按低 5 位处理。~x 按位取反,<< 左移,>> 保留符号右移。它们适合位标记等明确的 32 位任务,不能把 x | 0 当作任意大数的 Math.trunc(x)。

BigInt 从精确输入开始 ​

js
const id = BigInt('9007199254740993');
console.log(id + 2n); // 9007199254740995n
console.log(7n / 2n); // 3n:朝零截断
console.log(1n === 1, 1n == 1); // false true
console.log(BigInt.asUintN(8, 257n)); // 1n

BigInt(9007199254740993) 接收的 Number 已经失真,应从字符串或 n 字面量开始。Number 与 BigInt 的算术不能直接混合,先明确转换方向;比较大小可以跨两种类型。BigInt 没有小数,BigInt(1.5) 抛 RangeError,不是静默取整;无效字符串也会抛错。

BigInt 支持多数整数算术和位运算,不支持一元 + 和无符号右移 >>>。BigInt.asIntN/asUintN 按指定宽度截断为有符号或无符号整数。Math 方法不直接接受 BigInt;JSON.stringify 默认也不能序列化它,第三章讨论编码约定。

练习 ​

  1. 为什么 Number.isInteger 不能独自验证一个数据库大整数 ID?输入应以什么形式保留?

  2. 预测 Math.floor(-2.8)、Math.trunc(-2.8)、9n / 4n,再说明 BigInt(2.8) 的行为。

  3. 为什么0.5+0.25可以精确而0.1+0.2不一定?“用toFixed再相加”和“把EPSILON当所有尺度阈值”分别错在哪里?

  4. 数量输入约定为字符串:允许两端空白,数值必须是0~100的整数,空白输入无效。写出转换及校验程序,分别替换输入为 '0'、' 12 '、''、'12本'、'2.5'、'101'。不要用parseInt截掉无效尾部;再说明 '1e2' 按这份契约是否应通过。

查看答案

用心记录每一步成长