For AI agents: the complete documentation index is available at /tc39-atlas/llms.txt, the full documentation bundle is available at /tc39-atlas/llms-full.txt, and this page is available as Markdown at /tc39-atlas/proposals/stage/1/proposal-decimal.md.
  • 简体中文
  • Decimal S1

    中文标题:十进制数

    提案概览
    提案速览

    该提案针对 JavaScript 缺乏精确十进制算术的问题,二进制浮点数会导致用户可见的舍入错误。它计划基于 IEEE 754-2019 Decimal128 数据模型添加 Decimal 类型,提供算术、舍入、比较和转换等方法,且在 v1 中不引入字面量语法或运算符重载。

    Note

    以下 README 来自上游仓库,其中的阶段或状态标注可能滞后;当前信息以提案概览为准。

    Ecma TC39 JavaScript 十进制数提案

    TC39 十进制数提案旨在为 JavaScript 添加精确的十进制算术,以消除二进制浮点数计算中出现的舍入误差。

    有两种方式可看出这一提案的重要性。实际的角度是,使用二进制浮点数进行本意是十进制算术的计算时,会产生用户可见的 bug。另一个更为根本:我们希望有一种数值类型,其行为符合我们所学的算术规则:0.1 + 0.2 就是 0.3。十进制数本质上是“按我所想”(DWIM)的数值处理方式:数值与日常思维模型一致,没有隐藏的二进制表示怪癖。

    提案维护者欢迎您通过上述链接参与设计空间的讨论。我们正通过这份调查问卷征集您对 JavaScript 十进制数的需求。

    维护者(Champions)

    • Jesse Alama (Igalia)
    • Jirka Maršík (Oracle)
    • Andrew Paprocki (Bloomberg)

    作者(Authors):Jesse Alama, Waldemar Horwat

    阶段(Stage)TC39 流程的第 1 阶段。已有规范草案

    使用场景与目标

    在 JavaScript 中,准确存储和处理基数为 10 的小数是频繁的需求。目前,开发者有时使用专门的库来表示这些数字,有时使用字符串。遗憾的是,有时也会使用 JavaScript 的 Number,从而导致真实的、用户可见的舍入错误。

    问题是什么?为什么 0.1 + 0.2 不等于 0.3?JS 的 Number 在何种意义上不“精确”?JavaScript 的 Number 怎么可能出错,而且已错了这么多年?

    目前 JavaScript 中的 Number 是 64 位二进制浮点数。大多数十进制值到二进制浮点的转换很少能精确匹配。例如:十进制数 0.5 可以精确地用二进制表示,但 0.1 不可以;实际上,对应 0.1 的 64 位浮点数是 0.1000000000000000055511151231257827021181583404541015625。0.2、0.3 等也是如此……统计上,大多数人类书写的十进制数都无法精确表示为二进制浮点数(简称 float)。

    十进制数提案的目标是在 JavaScript 标准库中添加对十进制数的支持,提供良好的易用性和功能性。JS 程序员在合适的情况下应能舒适地使用十进制数。

    用于金融计算的精确十进制算术

    许多货币倾向于用十进制数量表达。虽然可以用整数“分”(将所有数量乘以 100)来表示金钱,但这种方法会遇到几个问题:

    • 人类对金钱的思考方式与程序处理方式之间存在持续的不匹配,给意识到该问题的程序员带来心智负担。
      • 有些程序员可能甚至没有意识到这种不匹配,从而为来源不明的舍入错误打开了大门。一旦计算变得复杂,出错的可能性就会增加。
    • 不同货币使用不同的小数位数,容易混淆;将隐含乘以 100 的数量进行处理的方法在处理多种货币时可能失效。例如,假设所有货币都有两位小数,或者唯一的例外是日元(JPY)是不正确的;这样的假设将使代码难以国际化到新的国家。
    • 有时还需要表示小数分的数量(例如,股票交易或货币兑换中的精确价格)。
    示例代码

    在接下来的示例中,我们将使用 Decimal 对象。

    将账单项目相加,再加上销售税
    function calculateBill(items, tax) {
      let total = new Decimal(0);
      for (let { price, count } of items) {
        total = total.add(new Decimal(price).times(new Decimal(count)));
      }
      return total.multiply(tax.add(new Decimal(1)));
    }
    
    let items = [
      { price: "1.25", count: 5 },
      { price: "5.00", count: 1 },
    ];
    let tax = new Decimal("0.0735");
    let total = calculateBill(items, tax);
    console.log(total.toFixed(2));
    货币兑换

    让我们将美元(USD)兑换为欧元(EUR),给定汇率 EUR --> USD。Decimal 进行精确计算;然后我们将结果交给 Amount 进行舍入以便显示,附加货币单位,并进行本地化。

    // 精确算术是 Decimal 的职责:
    let exchangeRateEurToUsd = new Decimal("1.09");
    let amountInUsd = new Decimal("450.27");
    let exchangeRateUsdToEur = new Decimal("1").divide(exchangeRateEurToUsd);
    let amountInEur = amountInUsd.multiply(exchangeRateUsdToEur);
    
    // 展示是 Amount 的职责(精度 + 货币单位 + 区域设置):
    let display = new Amount(amountInEur.toString(), {
      unit: "EUR",
      fractionDigits: 2,
    });
    console.log(display.toLocaleString("de-DE")); // "413,09 €"

    目前,两种类型通过 toString() 进行桥接;两个提案之间自然的协调点在于让 Amount 直接接受 Decimal 值,这样交接就变成 new Amount(amountInEur, { unit: "EUR", fractionDigits: 2 })

    为什么在此场景使用 JavaScript?

    历史上,人们可能认为 JavaScript 不适合精确十进制数的表示,因为进行任何计算都必然传播最初表示数值时产生的舍入误差。在某些应用架构中,JS 仅处理表示人类可读十进制数量的字符串(如 "1.25"),从不进行计算或转换。然而,一些趋势促使 JS 更深入地参与十进制数量的处理:

    • 更复杂的前端架构:为了获得更好的交互性能,舍入、本地化或其他展示方面的工作可能在[前端]{.品牌}执行。
    • 无服务器(Serverless):许多无服务器系统使用 JavaScript 作为编程语言,以便更好地利用前端工程师的知识。
    • JavaScript 服务端编程:Node.js 和 Deno 等系统日益流行,使得 JavaScript 能够进行更传统的服务端编程。

    在上述所有环境中,缺乏十进制数支持意味着必须采用各种变通方法(再次强调,前提是程序员甚至意识到 JS 内置二进制浮点数与真正的十进制数之间存在固有差距):

    • 可以使用外部库(这带来选择库、协调其使用的问题)。
    • 可以以“分”为单位进行计算(如前所述,容易出错)。
    • 在某些情况下,开发者最终仍会使用 Number——明知其局限性,或认为其大体安全——但实际上会导致 bug,即使他们尝试处理舍入或从十进制到二进制浮点的非精确转换。

    换句话说,随着 JS 越来越多地用于传统上未涉足的上下文和场景,对原生处理其他系统已能原生处理的基本数据(如十进制数)的需求不断增长。

    处理来自其他系统的十进制数据

    JavaScript 通常是连接系统的“胶水”——数据库、外部函数接口、其他服务——这些系统原生支持十进制数。Decimal 允许您获取这些数据并对其进行精确计算,保留其数学值。

    注意,Decimal 保留的是,而非表示形式:根据 "1.20" 构建的 Decimal 与根据 "1.2" 构建的无法区分(参见数据模型)。因此,如果您只需原样传输值,字符串即可无损地做到。如果您需要跨边界携带声明的精度或小数位数,那是 Amount 的职责。一旦您需要计算并保留数学值,Decimal 便发挥其作用。

    以下示例说明了这一想法。注意 sql_decimal 配置选项,以及从数据库返回的值以 Decimal 值形式到达——准备好进行精确算术——而不是字符串或 JS Number

    const { Client } = require("pg");
    
    const client = new Client({
      user: "username",
      sql_decimal: "decimal", // or 'string', 'number'
      // ...more options
    });
    
    const boost = new Decimal("1.05");
    
    client.query("SELECT prices FROM data_with_numbers", (err, res) => {
      if (err) throw err;
      console.log(res.rows.map((row) => row.prices.times(boost)));
      client.end();
    });
    此使用场景隐含的目标

    此使用场景隐含以下目标:

    • 精确算术:加法、减法、乘法和除法计算其精确的十进制结果,无意外舍入导致用户可见错误
    • 当计算需要舍入时,显式、选择性地舍入,并提供舍入模式的选项
    • 足够的精度以处理典型的人类可读的数量,并在算术过程中保持精度
    • 足够的易用性以确保正确使用
    • 在应用程序中能以足够的性能/内存使用量实现
    • (请提交 issue 以提出更多需求)

    语言设计目标

    除上述使用场景直接带来的目标外:

    • 清晰的语义,无论代码在何种实现和上下文中运行,结果都一致
    • 与 Number、BigInt、运算符重载以及潜在的未来内置数值类型一起,为 JavaScript 建立一致的数值处理方案
    • 运算符语义中不涉及全局可变状态;也反对动态作用域的状态
    • 能够在所有 JavaScript 环境(例如嵌入式、服务器、浏览器)中实现

    与 Web 平台其他部分的交互

    如果 Decimal 成为标准 JavaScript 的一部分,它可能会在宿主环境的一些内置 API 中使用:

    • 对于 Web 平台: (#4)
      • HTML 序列化将支持 Decimal,就像支持 BigInt 一样,因此 Decimal 可用于 postMessageIndexedDB 等。
    • 对于 WebAssembly,如果 WebAssembly 未来添加 IEEE 64 位和/或 128 位十进制标量类型,那么 WebAssembly/JS API 可以引入边界处的转换,类似于 WebAssembly BigInt/i64 集成

    更多宿主 API 交互在 #5 中讨论。

    先例

    添加十进制算术并非 JS 的开创之举。许多主流编程语言已经认识到十进制算术足够基础,应包含在标准库中,甚至作为原始类型。

    各语言对十进制的支持

    语言类型名位置加入年份备注
    Pythondecimal.Decimal标准库2003 (Python 2.3)基于通用十进制算术规范
    Javajava.math.BigDecimal标准库1998 (JDK 1.1)任意精度,广泛用于金融计算
    C#decimal原始结构类型2000 (C# 1.0)128 位十进制类型,一等语言支持
    SwiftDecimal (原名 NSDecimalNumber)Foundation 框架2016 (Swift 3.0)用于金融计算的标准库类型
    RubyBigDecimal标准库1999 (Ruby 1.6)任意精度十进制算术

    SQL 也有内置的 NUMERIC/DECIMAL 类型,早于许多编程语言。

    许多语言在 10 多年前就添加了十进制支持。IEEE 754-2008 标准的 Decimal128 已经有 17 年的历史。(IEEE 754 的 2019 版也有十进制算术。)

    此外,在语言中拥有多种数值类型并不罕见。语言通常带有整数、浮点数小数。(有些甚至有更多数值类型,例如内置分数支持。)Python 甚至同时包含十进制和分数。JS 中存在 NumberBigInt 并不妨碍 Decimal 的存在。而且,许多语言对十进制类型的需求反映了一种真实、普遍的需求,而非小众需求。

    更全面的十进制及相关类型调查

    上表刻意简短。实际上,十进制、分数及类似特性在标准、编程语言、数据库、库甚至硬件中普遍存在。以下是部分目录,并附有每种语义的简要总结。

    • 标准
      • IEEE 754-2008 及后续版本:32 位、64 位和 128 位十进制;参见解释(建议不要使用 32 位十进制)
      • (此外,以下许多项目也已标准化)
    • 编程语言
      • 固定大小十进制:
        • C:32、64 和 128 位 IEEE 754 十进制类型,带全局设置对象。仍是提案,但有 GCC 实现。
        • C++:早期提案工作仍在进行,将基于 IEEE 64 位和 128 位十进制。仍是提案,但有 GCC 实现。
        • C#/.NET:自定义 128 位十进制语义,与 IEEE 相比,尾数和指数的位数略有不同。
        • Swift/Obj-C:另一种固定位大小浮点小数的自定义语义。
      • 设置十进制精度的全局设置
        • Python:带全局设置以设置精度的 Decimal。
      • 分数
        • Perl6:像 1.5 这样的字面量是 Rat 实例!
        • Common Lisp:Ratios 与浮点数并存;没有十进制数据类型
        • Scheme:与 Common Lisp 类似,但类型名称不同(Racket 也类似)
        • Ruby:与 BigDecimal 并列的 Rational 类。
      • 任意精度十进制(本提案)
        • Ruby:任意精度 Decimal,与 Rational 并列。
        • PHP:一组绑定 bc 用于数学计算的函数。还有一个社区驱动的替代 Decimal 库
        • Java:基于对象和方法的任意精度十进制。对于除法等操作,需要舍入模式和精度参数。
    • 数据库
      • Intel C inteldfp:IEEE 十进制
      • Bloomberg C++ bdldfp:IEEE 十进制
      • IBM C decnumber:带精度、舍入模式的可配置上下文
      • Rust crates [1] [2]
    • 硬件(均实现 IEEE 十进制)

    为什么 JavaScript 缺乏十进制支持

    JavaScript 作为轻量级浏览器脚本语言的起源意味着它最初只提供最低限度的数值支持(仅 IEEE 754 二进制浮点)。然而,JavaScript 的角色已发生根本性变化。在 20 世纪 90 年代,JS 专注于表单验证和 DOM 操作。如今我们有了全栈应用,包括金融系统、电子商务平台、无服务器后端和数据流水线。语言已演进以满足现代需求,但十进制算术仍是一个重要缺口。

    没有 Decimal 的代价

    由于 JavaScript 缺乏原生十进制支持,开发者创建了众多用户态的解决方案:

    还有专用于货币的库,如 dinero.js,每周约 18 万次 npm 下载。每个实现都有略微不同的语义,需要在库之间协调,增加总体积,可能比原生实现性能差,并带来互操作性挑战。我们相信 Decimal 将标准化现有的实践:开发者已经依赖这些库,或退回到上文描述的错误易发变通方法。Decimal 并不要求开发者采用新事物;它提供了一种更好的方式来完成他们已经在努力做的事情。

    上述三个最流行的库均由 MikeMcl 编写。它们有一些有趣的差异,但也具有共同的形式:对象加方法的 API,舍入模式和精度限制在构造函数上配置,并支持固有的舍入运算,如平方根、乘方和除法。我们计划随着对 Decimal 设计的完善,从开发者使用这些及其他生态库的经验中学习;讨论在 #22 中继续。

    规范与标准

    基于 JS 开发者、引擎实现者和 TC39 委员会成员的反馈,我们有了具体的提案。请参阅规范文本渲染 HTML)。我们鼓励您通过评论下表列的 issue 或提交您自己的 issue 参与讨论。

    我们将采用 Decimal128 数据模型作为 JavaScript 的十进制数。Decimal128 并非新标准;它于 2008 年被添加到 IEEE 754 浮点算术标准中(并存在于 JS 规范依赖的 2019 版 IEEE 754 中)。它代表了数十年十进制浮点数研究的成果。Decimal128 宇宙中的值占用 128 位。在这种表示中,最多可存储 34 位有效数字(即十进制数字),指数(10 的幂)为 +/- 6143。

    已知的替代方案

    无限精度十进制(又称 "BigDecimal")

    "BigDecimal" 数据模型基于无限大小的十进制值(无固定位宽),理解为精确的数学值。

    从维护者组的角度来看,BigDecimal 和 Decimal128 都是连贯、有效的提案,都能满足主要使用场景的需求。仅仅看看其他编程语言语义的多样性,以及程序员遇到的实际问题不多,就可以看出这里有许多可行的答案。

    运算符始终计算精确结果。特别是,如果两个 BigDecimal 相乘,结果的精度可能高达操作数的总和。因此,BigDecimal.pow 需要一个必需的选项对象,以确保结果的精度不会失控。

    人们可以设想任意精度的十进制版本,我们也探索过这条路;历史信息可参阅 bigdecimal-reference.md

    BigDecimal 的一个困难是,除法不能作为双参数函数使用,因为通常需要舍入参数。需要一个 BigDecimal.div 函数,其中某些选项是必需的。有关 BigDecimal 中除法的进一步讨论,参见 #13

    关于 BigDecimal 与 Decimal128 之间权衡的进一步讨论可在 #8 中找到。

    固定精度十进制

    想象每个十进制数在小数点后固定有,例如,十位数字。任何需要,例如,十一位小数的情况将无法表示。这就是固定精度的小数世界。数字十只是一个例子;需要进行一些研究才能确定一个好的数值。甚至可以想象这类数字的精度可以参数化。

    有理数

    有理数,又称分数,提供了一种邻近的方法。从数学角度来看,有理数比小数更具表达力:每个小数都是一种分数(带符号整数除以 10 的幂),而某些有理数(如 1/3)则不能(有限地)表示为小数。那么为什么不使用有理数呢?

    • 分子和分母的大小在进行操作时通常呈指数增长。仅进行一次乘法或除法通常会导致有理数各部分的大小倍增。即使是加法和减法也会导致快速增长。这意味着为有理数提供的精度付出了高昂的代价。
    • 必须警惕分子和分母的规范化,这涉及重复计算 GCD、将它们除以 GCD 并继续。另一种选择是不规范化有理数,例如每进行五次算术操作后规范化。这可能是一项昂贵的操作,当然比将 "1.20" 规范化为 "1.2" 昂贵得多。
    • 各种操作(如乘方和对数)给定有理数参数几乎从不产生有理数,因此必须在这些操作中指定一定精度作为第二个参数。相比之下,在 Decimal128 中,这些操作不需要第二个参数。

    分数将是 TC39 值得追求的有趣话题,并且在许多方面与 Decimal 互补。有理数的用例与十进制数的用例有部分重叠。许多 Lisp 传统的语言(例如 Racket)将分数作为基本数据类型,与 IEEE 754 64 位二进制浮点数并列;Ruby 和 Python 的标准库中也包含分数。

    我们认为有理数与 Decimal 是互补的,因为在 Decimal 的两个核心操作上存在不匹配:

    • 以基数 10 的精度、带舍入模式进行舍入
    • 转换为本地化、人类可读的字符串

    这些_可以_定义在有理数上,但由于有理数不是基数为 10 的,所以存在固有的不匹配。

    有理数可能仍然作为独立的数据类型与 Decimal 并列存在。关于有理数的进一步讨论见 #6

    语法与语义

    对于 Decimal,我们不设想新的字面量语法——比如 123.456_789m 后缀(#7)。根据 JS 引擎实现者在多次 TC39 全体会议上的反馈,我们选择不添加它。关于 v1 如何与未来添加字面量兼容,请参阅“未来工作”下的字面量

    数据模型

    如上文规范与标准所述,Decimal 采用 IEEE 754-2019 Decimal128 数据模型。我们将提供官方 Decimal128 的子集。具体包括:

    • 单个 NaN 值——与 JS 内置的 NaN 不同。静默 NaN 和信号 NaN 的差异将合并为单个(静默)Decimal NaN。
    • 正无穷大和负无穷大将被提供,但同样与 JS 内置的 Infinity-Infinity 不同。
    • 负零(-0)是区别于正零的值,与 IEEE 754 和 JavaScript 的 Number 一致;两者比较相等。

    这些特殊值以普通 Decimal 值的形式暴露,而不是抛出异常,以便在异常情况下计算可以继续。

    Decimal 不暴露尾随零的信息。因此,"1.20" 是有效语法,但无法区分 1.20 与 1.2:相等比较数学值(因此 1.201.2 相等),而 toString 始终渲染规范化的字符串。关键是,这是关于 Decimal 暴露了什么的保证,而不是对值如何存储的要求。实现可以自由地在内部保持值未规范化,并仅在输出时(渲染字符串或比较值时)进行规范化。一个能概括这一点的座右铭是“在输出时规范化”。

    这种混合立场是深思熟虑的。Mike Cowlishaw,IEEE 754 十进制算术的作者之一,在十进制算术 FAQ 中解释了为什么十进制算术通常以非规范化方式执行:加法和减法通常不需要对齐移位,系数和指数计算保持独立,结果符合用户对手动计算的期望。由于 Decimal 永不暴露值是否规范化,实现可以获得几乎所有这些好处(主要是性能),而无需强制调用者考虑尾随零。反过来,不暴露尾随零是 IEEE 754 定义的能力中的刻意省略:尾随零是显示精度,而非算术,跟踪它们正是 Amount 的职责。(相关地,保留尾随零提案现处于第 3 阶段,确保 Intl 在格式化数字字符串时不会静默剥离尾随零。)

    运算符语义

    • 算术
      • 一元运算
        • 绝对值
        • 取反
      • 二元运算
        • 加法
        • 乘法
        • 减法
        • 除法
        • 余数
    • 舍入:支持 IEEE 754 的所有五种舍入模式——向下取整、向上取整、截断、四舍五入至偶数、四舍五入远离零。
      • (这意味着 Intl.NumberFormatTemporal 中的一些舍入模式将不受支持。)
    • 比较
      • 等于和不等于
      • 小于、小于或等于
      • 大于、大于或等于
    • 尾数、指数、有效数字

    此处提供的数值函数库刻意保持最小化。它基于主要使用场景,即相当直接的计算。对于科学或数值计算,开发者可以在 JavaScript 中试验,开发此类库,我们可能决定在后续提案中将这些函数标准化;将提供最小化的尾数、指数和有效数字工具包。我们目前对该使用场景的开发者需求没有很好的洞察,除了泛泛而言:可能需要平方根、乘方和对数、三角函数,但我们不确定这是否是完整列表,以及哪些比其他更重要。同时,人们可以使用 JavaScript Math 标准库中的各种函数。

    不支持的操作

    • 不支持位运算符,因为它们在 Decimal 域上逻辑上无意义(#20
    • 我们目前不设想 Decimal 值与其他 Number 值交互。也就是说,当尝试将一个 Number 加到 Decimal 上时会抛出 TypeError,类似于 BigInt 与 Number 的情况。(#10)。

    与其他数据类型的转换

    Decimal 对象可以从 Number、String 和 BigInt 构建。同样,也有从 Decimal 对象到 Number、String 和 BigInt 的转换。

    字符串格式化

    Decimal 对象可以以多种方式转换为简单的、与区域设置无关的字符串,类似于 Number:

    • toString() 的行为与 Number 类似,例如 new Decimal("123.456").toString()"123.456"。(#12
    • toFixed() 类似于 Number 的 toFixed()
    • toPrecision() 类似于 Number 的 toPrecision()
    • toExponential() 类似于 Number 的 toExponential()

    我们打算将这些方法定义为抽象操作,将数学值渲染为数字字符串(固定、精度限制或指数形式)。这些抽象操作的编写方式使得 Amount 提案 能够重用它们,从而为 Decimal 和 Amount 提供单一、一致的小数值转字符串的概念。

    Decimal 还支持感知区域设置的格式化:Decimal.prototype.toLocaleString 为 Decimal 值生成本地化字符串,类似于 Number.prototype.toLocaleString#15)。这是 Web 上的基本需求,因此 Decimal 直接提供它,而无需另一种类型。

    Decimal 和 Amount 被设计为共享底层格式化机制而非重复实现;toLocaleString 是通过内部构建 Amount 还是通过通用抽象操作来指定,是规范中待定的实现细节。关于何时使用哪种类型,请参见下文Amount

    TC39 全体会议中的过往讨论

    Decimal 在 TC39 中已讨论很长时间,许多人都提出过提案和反馈,包括 Sam Ruby、Mike Cowlishaw、Brendan Eich、Waldemar Horwat、Maciej Stachowiak、Dave Herman 和 Mark Miller。最早的讨论线程先于现代提案流程:

    • 一种新的 decimal 类型早已为 ES4 计划,参见拟议的 ECMAScript 第 4 版 – 语言概述
    • 在随后的 ES3.1/ES5 工作中,关于 decimal 类型的讨论在 es-discuss 上继续,例如 [1] [2]
    • 在 ES6 开发中,Decimal 被广泛讨论。最终被纳入更广泛的类型对象/值类型工作中,但未进入 ES6,而是目前正在逐步开发。

    近期,Decimal 多次出现在 TC39 全体会议议程上:

    未来工作

    这里描绘的 Decimal 愿景代表了维护者当前的想法和目标。在我们看来,到目前为止所描绘的 Decimal 是对语言的一个宝贵补充。尽管如此,我们设想改进并努力在提案的 v2 中实现这些改进。以下内容_不是_当前提案的一部分,但我们正努力使第一个版本与这些未来添加兼容。

    算术运算符和比较重载

    在早期关于 Decimal 的讨论中,我们主张支持算术运算(+* 等)和比较(==< 等)以及 === 的重载。但根据实现者的强烈反馈,我们决定采用以下提案:

    • 在本提案的第一个版本中,我们打算让 +* 等运算在任一参数为 decimal 值时抛出异常。相反,必须使用 addmultiply 等方法。同样,比较运算符如 ==<<= 等也会在任一参数为 decimal 时抛出异常。应改用 equalslessThan 方法。
    • 严格相等运算符 === 将正常工作(不会抛出异常),但将具有默认的对象语义;不会涉及 decimal 值的任何特殊处理。

    然而,重载的大门并非_永久_关闭。只是将其添加到 JS 的门槛非常高。如果我们获得足够多的积极开发者反馈,并与实现者合作找到前进的道路,我们或许能够达到那个门槛。

    字面量

    在本提案的早期讨论中,我们曾主张向语言添加新的 decimal 字面量:1.289m(注意小写的 m 后缀)。确实,由于 decimal 是数字——本质上类似于现有二进制浮点数的基础数据——在语法中为它们提供自己的“空间”是相当合理的。

    然而,与运算符重载类似,我们收到了实现者的强烈反馈,认为这不太可能发生。

    尽管如此,我们正在努力确保此处所述的提案 v1 版本与 future 中存在 decimal 字面量的情况兼容。与运算符重载一样,需要与 JS 引擎实现者保持开放讨论,以找出可以添加此功能的方法。(假设存在 decimal 的 v1,可以使用 Babel 转换相当直接地添加字面量支持。)

    高级数学函数

    在我们的讨论中,我们一直强调基本算术的必要性。而在提案的 v1 中,我们实际上就止步于此。可以想象 Decimal 拥有 Math 标准库对象的所有能力,包含以下数学函数:

    • 三角函数(普通、反/弧、双曲组合)
    • 自然指数和对数
    • 还有其他吗?

    这些可以更直接地在 Decimal 的 v2 中添加。根据我们已收到的开发者反馈,我们感觉到对这些函数的需求相对较少。但一旦 Decimal 的 v1 被广泛使用,此类反馈的出现并非不合理。

    常见问题解答

    那有理数呢?

    参见上文关于数据模型的讨论,其中有理数已被讨论。

    Decimal 会有良好的性能吗?

    这取决于实现。与 BigInt 一样,实现者可以决定是否优化以及优化哪些场景。我们相信,无论哪种选择,都有可能创建高性能的 Decimal 实现。历史上,面对 BigInt 与 Int64 的类似决策,TC39 选择了 BigInt;由于用例差异,这种决策可能不会完美对应。进一步讨论: #27

    Decimal 在不同实现和环境中的行为会一致吗?

    有人提出的一种选项是在能力更强的环境中允许更高的精度。然而,Decimal 的核心在于避免非预期的舍入。如果舍入行为取决于环境,那么在这些环境中目标就会妥协。相反,本提案试图找到一组可以全局应用的单一语义。

    本提案与运算符重载等其他 TC39 提案有何关系?

    运算符重载在未来工作中涵盖:Decimal 的第一个版本刻意重载 +< 等运算符,尽管大门并未永久关闭。与 BigInt、Amount 及其他相关提案的关系在Decimal 与其他 TC39 提案的关系中涵盖。

    为什么不设置环境决定的最大精度或默认舍入模式?

    许多十进制实现支持全局选项来设置最大精度(例如 Python、Ruby)。在 QuickJS 中,存在“动态作用域”版本:setPrec 方法在特定函数运行期间更改最大精度,并在返回后恢复。默认舍入模式也可以类似地设置。

    尽管动态作用域版本更为受限,但两种版本都是反模块化的:代码行为不独立,而是依赖于调用它的周围代码。一个可靠的库必须始终在其周围设置精度。

    在 JavaScript 的多全局/Realms 方面还有进一步复杂性:Decimal 原始值与任何全局无关,因此无法将状态存储在那里。必须跨系统中的所有 Decimal 实例存储。但这会形成一个跨 Realm 的通信通道。

    因此,本提案不包含任何从环境设置精度的选项。

    我可以在哪里了解更多关于十进制数的知识?

    Mike Cowlishaw 的出色 Decimal FAQ 解释了十进制数据类型的一些核心设计原则,本提案力求遵循这些原则。

    其中一个原则——十进制算术最自然地在非规范化值上执行——塑造了 Decimal 的“在输出时规范化”立场,这已在数据模型下讨论。

    Decimal 与其他 TC39 提案的关系

    BigInt

    本提案可视为 BigInt 的后续,后者为 JavaScript 带来了任意大小的整数,并已于 ES2020 标准化。然而,与 BigInt 不同,Decimal (i) 不引入新的基本数据类型,(ii) 不提议运算符重载(BigInt 支持),(iii) 不提供新语法(数字字面量),而 BigInt 确实添加了(例如 2345n)。

    Amount

    Decimal 旨在与 TC39 Amount 提案(第 2 阶段)协同工作。两个提案按以下方式划分处理十进制数量的任务:

    Decimal 计算值

    Amount 携带值的精度和单位。

    Amount 将数值与测量上下文(有效数字或小数位数)以及可选单位(包括货币)包装在一起。Amount 刻意执行算术(最多在特定数字位数后舍入)。相反,Decimal 刻意不跟踪显示精度,也没有单位概念。Decimal 值可以通过 toLocaleString 以区域敏感方式渲染。Amount 添加精度和单位/货币数据,它可以接受 Decimal 来实现。

    关注点所有者
    精确的 +×÷、余数和舍入Decimal
    比较Decimal
    提取尾数/指数/有效数字Decimal
    处理有效/小数位数(包括尾随零)Amount
    单位和货币Amount
    普通字符串渲染(toStringtoFixedtoPrecisiontoExponentialDecimal(通过 Amount 可重用的共享渲染 AOs)
    区域感知格式化(toLocaleString裸数字用 Decimal;涉及单位、货币或显示精度时用 Amount
    复数选择Amount(Decimal 可能丢失相关数据)
    单位转换Amount

    其他相关提案

    Decimal 和 Amount 属于一个小的 TC39 提案家族,共同覆盖处理十进制数量的工作。在 Amount 之外:

    • 保留尾随零(第 3 阶段):确保 Intl 在格式化数字字符串时不会静默剥离尾随零(例如,意外地将 "1.20" 规范化为 "1.2")。
    • 智能单位:Amount 的后续,用于感知区域设置和使用场景的单位偏好与转换。

    实现

    • QuickJS 中的实验实现,自 2020-01-05 版本起(使用 --bignum 标志)
    • decimal128.js 是一个 npm 包,用 JavaScript 实现 Decimal128(更准确地说,是我们为本提案设想的 Decimal128 变体)
    • 我们正在寻找志愿者,为两种替代方案编写类似 JSBI 的 polyfill,参见 #17

    参与本提案

    我们非常期待您对本提案的帮助!参与方式有很多种:

    • 问题跟踪器上分享您的想法
    • 记录您的使用场景,并编写使用 decimal 的示例代码,在 issue 中分享
    • 研究 JS 生态系统中 decimal 的当前使用情况,并在 issue 中记录什么有效、什么无效
    • 帮助我们编写和改进文档、测试和原型实现

    查看完整的待办任务列表,请见 #45