Skip to content

核心概念

理解这 6 个概念,你就掌握了 decimal.js 的设计思想。本文是后续 API 章节的理论基础。

1. 为什么需要它:Number 的局限

JavaScript 原生 Number 是 IEEE 754 双精度浮点数,有两个先天问题:

问题 A:二进制无法精确表示部分十进制小数

js
0.1 + 0.2                    // 0.30000000000000004
0.3 - 0.1                    // 0.19999999999999998

问题 B:整数也有上限

js
Number.MAX_SAFE_INTEGER       // 9007199254740991(2^53 - 1)
9007199254740993 + 1          // 9007199254740992(精度丢失!)

decimal.js 用十进制存储每一位数字,从根本上规避这两类问题:

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 模块,而非仅除法舍入):

js
new Decimal(1).div(3)          // '0.33333333333333333333'(20 位有效数字)

不超过精度的结果完整保留:

js
new Decimal(123456789).plus('0.000000001')
// '123456789.000000001'(18 位有效数字 ≤ 20,一位不丢)

超过精度的结果按舍入模式截断(末尾用 0 补足位数):

js
new Decimal('123456789012345678901').plus(1)
// '123456789012345678900'(21 位有效数字 → 舍入为 20 位)

再举一个更直观的例子:

js
Decimal.set({ precision: 5 });
new Decimal('12345').plus('0.5')     // '12346'(12345.5 有 6 位有效数字,四舍五入)

3. 舍入模式(rounding)

当结果的有效数字超过 precision 时,按舍入模式决定如何截断。默认 rounding = 4(ROUND_HALF_UP,四舍五入)。

常量行为
ROUND_UP0远离零(绝对值增大)
ROUND_DOWN1趋向零(截断)
ROUND_CEIL2趋向 +∞(向上取整)
ROUND_FLOOR3趋向 -∞(向下取整)
ROUND_HALF_UP4四舍五入(默认)
ROUND_HALF_DOWN5五舍六入(.5 向下)
ROUND_HALF_EVEN6银行家舍入(.5 取偶数)
ROUND_HALF_CEIL7.5 时趋向 +∞
ROUND_HALF_FLOOR8.5 时趋向 -∞

每种模式的完整解释与示例见 配置与舍入模式

4. 指数与字符串输出规则

Decimal 内部按「系数 × 10^指数」存储。字符串化(toString)时是否使用科学计数法,由两个配置决定:

  • toExpPos(默认 21):指数 ≥ 该值时用科学计数法
  • toExpNeg(默认 -7):指数 ≤ 该值时用科学计数法
js
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()

js
new Decimal('0.0000001').toString()   // '1e-7'
new Decimal('0.0000001').toFixed()    // '0.0000001'

5. 不可变性(Immutable)

所有方法都不修改原对象,而是返回新值。这保证了链式调用和共享引用的安全:

js
const x = new Decimal(0.3);
x.minus(0.1);      // '0.2'
x;                 // '0.3',x 未变

内部虽有 d(数字数组)、e(指数)、s(符号)三个属性,但应视为只读,不要直接修改:

js
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 或 -1

6. Number 输入的精度陷阱

new Decimal(number) 时,number 先被转成它的十进制字符串再解析。精度损失发生在 Number 本身(IEEE 754 早已失真),decimal.js 无法挽回:

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'(运算结果本身就不精确)

但注意:普通字面量是安全的,因为其十进制字符串表示就是字面量本身:

js
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 的精度损失无法修复
输出toStringtoExpNeg/toExpPos 决定是否用科学计数法