核心概念
理解这 6 个概念,你就掌握了 decimal.js 的设计思想。本文是后续 API 章节的理论基础。
1. 为什么需要它:Number 的局限
JavaScript 原生 Number 是 IEEE 754 双精度浮点数,有两个先天问题:
问题 A:二进制无法精确表示部分十进制小数
0.1 + 0.2 // 0.30000000000000004
0.3 - 0.1 // 0.19999999999999998问题 B:整数也有上限
Number.MAX_SAFE_INTEGER // 9007199254740991(2^53 - 1)
9007199254740993 + 1 // 9007199254740992(精度丢失!)decimal.js 用十进制存储每一位数字,从根本上规避这两类问题:
new Decimal('0.1').plus('0.2') // '0.3'
new Decimal('9007199254740993').plus(1) // '9007199254740994'2. 精度以「有效数字」计(significant digits)
这是 decimal.js 与 big.js / bignumber.js 最大的设计差异:精度单位是有效数字,不是小数位。
- 有效数字:从第一个非零数字起,到最后一个数字(含末尾零)为止的位数。
12345→ 5 位;0.00012345→ 5 位;1.2300→ 5 位(字符串形式)。
- 小数位:小数点后的位数。
12345→ 0 位;0.00012345→ 8 位。
默认 precision = 20,即所有计算结果最多保留 20 位有效数字(类似 Python 的 decimal 模块,而非仅除法舍入):
new Decimal(1).div(3) // '0.33333333333333333333'(20 位有效数字)不超过精度的结果完整保留:
new Decimal(123456789).plus('0.000000001')
// '123456789.000000001'(18 位有效数字 ≤ 20,一位不丢)超过精度的结果按舍入模式截断(末尾用 0 补足位数):
new Decimal('123456789012345678901').plus(1)
// '123456789012345678900'(21 位有效数字 → 舍入为 20 位)再举一个更直观的例子:
Decimal.set({ precision: 5 });
new Decimal('12345').plus('0.5') // '12346'(12345.5 有 6 位有效数字,四舍五入)3. 舍入模式(rounding)
当结果的有效数字超过 precision 时,按舍入模式决定如何截断。默认 rounding = 4(ROUND_HALF_UP,四舍五入)。
| 常量 | 值 | 行为 |
|---|---|---|
ROUND_UP | 0 | 远离零(绝对值增大) |
ROUND_DOWN | 1 | 趋向零(截断) |
ROUND_CEIL | 2 | 趋向 +∞(向上取整) |
ROUND_FLOOR | 3 | 趋向 -∞(向下取整) |
ROUND_HALF_UP | 4 | 四舍五入(默认) |
ROUND_HALF_DOWN | 5 | 五舍六入(.5 向下) |
ROUND_HALF_EVEN | 6 | 银行家舍入(.5 取偶数) |
ROUND_HALF_CEIL | 7 | .5 时趋向 +∞ |
ROUND_HALF_FLOOR | 8 | .5 时趋向 -∞ |
每种模式的完整解释与示例见 配置与舍入模式。
4. 指数与字符串输出规则
Decimal 内部按「系数 × 10^指数」存储。字符串化(toString)时是否使用科学计数法,由两个配置决定:
toExpPos(默认 21):指数 ≥ 该值时用科学计数法toExpNeg(默认 -7):指数 ≤ 该值时用科学计数法
new Decimal('1e20').toString() // '100000000000000000000'(指数 20 < 21,普通形式)
new Decimal('1e21').toString() // '1e+21'
new Decimal('1e-6').toString() // '0.000001'
new Decimal('1e-7').toString() // '1e-7'不想看到指数形式?用 toFixed():
new Decimal('0.0000001').toString() // '1e-7'
new Decimal('0.0000001').toFixed() // '0.0000001'5. 不可变性(Immutable)
所有方法都不修改原对象,而是返回新值。这保证了链式调用和共享引用的安全:
const x = new Decimal(0.3);
x.minus(0.1); // '0.2'
x; // '0.3',x 未变内部虽有 d(数字数组)、e(指数)、s(符号)三个属性,但应视为只读,不要直接修改:
const x = new Decimal(-12345.67);
x.d // [ 12345, 6700000 ] —— 数字,按 10^7 分块存储(基数 10000000)
x.e // 4 —— 指数,x = 0.1234567 × 10^5 的量级
x.s // -1 —— 符号:1 或 -16. Number 输入的精度陷阱
new Decimal(number) 时,number 先被转成它的十进制字符串再解析。精度损失发生在 Number 本身(IEEE 754 早已失真),decimal.js 无法挽回:
new Decimal(1.0000000000000001) // '1'(字面量本身已被 JS 舍入为 1)
new Decimal(88259496234518.57) // '88259496234518.56'
new Decimal(99999999999999999999) // '100000000000000000000'(字面量被舍入为 1e20)
new Decimal(2e+308) // 'Infinity'(超出 Number 范围)
new Decimal(1e-324) // '0'(下溢)
new Decimal(0.7 + 0.1) // '0.7999999999999999'(运算结果本身就不精确)但注意:普通字面量是安全的,因为其十进制字符串表示就是字面量本身:
new Decimal(1.005).toString() // '1.005'(不是 1.00499999...)
new Decimal(1.005).toFixed(2) // '1.01'(按 '1.005' 四舍五入)WARNING
经验法则:超过 15 位有效数字,或者来自浮点运算结果的值,一律用字符串传入(见 最佳实践与常见坑 的坑 1、坑 2)。
7. 其他要点
- NaN / Infinity 是合法值:
new Decimal(NaN)、new Decimal(Infinity)都合法。 -0是合法的:new Decimal('-0')的isNegative()为true,且valueOf()返回'-0'而toString()返回'0'(见 格式化与输出)。- 所有计算都舍入:加、减、乘、除、开方、三角函数……只要结果超过精度,一律按
rounding舍入,这是与 bignumber.js 的重要区别。
小结
| 概念 | 要点 |
|---|---|
| 精度单位 | 有效数字,默认 20 位 |
| 舍入 | 所有运算结果统一按 rounding 舍入 |
| 不可变 | 方法返回新 Decimal,原值不变 |
| 存储 | 系数 d + 指数 e + 符号 s,只读 |
| 输入 | 优先字符串;Number 的精度损失无法修复 |
| 输出 | toString 按 toExpNeg/toExpPos 决定是否用科学计数法 |
