☰
JS金额格式化:千分位、两位小数与浮点精度避坑指南
2026/9/30 10:37:24 网站建设 项目流程

做后台管理系统的人大概都遇到过这个场景:财务同事甩过来一张截图,说金额 1234567.8 看着眼睛疼,能不能改成 1,234,567.80 这样一眼能数清零的。你心想这不就是 toFixed 加个正则嘛,十分钟搞定,结果上线第二天需求方又来了——1.005 显示成了 1.00,跟他们对账的表格对不上,差着五厘钱。于是你回头看代码,发现 toFixed 这个东西在浮点数面前根本靠不住。这就是 JS 格式化金钱这件"小事"的真实面貌:字符串加逗号(千分位)看着简单,保留两位小数看着简单,但两者叠在一起,再叠上负数、零、超大金额、用户手动输入、国际化符号,坑就一层一层出来了。这篇内容就是把这套东西从原理到代码完整拆一遍,前端、Node 端、做数据报表的都能直接拿去用,我会给出一份经过线上验证的格式化函数,以及一堆只有真写过才会知道的细节。

1. 金额格式化到底在解决什么问题

1.1 从三个真实业务场景看需求来源

金额格式化这个需求,几乎不会单独出现在需求文档里,它总是藏在别的功能背后。第一种是列表和详情页展示,后端接口返回的永远是原始的 number 或者 string,像1234567.8、-0.5、999999999999.999这种形态,前端必须自己转成人能读的格式。第二种是输入框,用户在录入合同金额、发票金额的时候,你得一边让他输一边把逗号补上去,同时限制他不能输三个小数点。第三种是导出和打印,Excel、PDF、对账单这种场景对格式的要求最死,位数不对、符号不对,财务那边直接打回来重做。

这三种场景的技术难点完全不同。展示场景只要求"好看且正确",纯函数就能解决;输入场景要多考虑光标位置和输入法状态,一不注意用户打字时光标就跳到末尾;导出场景则要考虑数字精度和符号位数,因为一旦导出成字符串,再拿回来做计算就会出错。很多人在第一个场景踩了坑,写了个带逗号的字符串存回数据库,后面全链路都得做反向解析,这是典型的自己给自己挖坑。

1.2 千分位和小数位的本质区别

很多人把这两件事当成一个功能来写,其实它们的性质差得很远。千分位是纯粹的视觉分组,它不改变数值本身,1234567和1,234,567在数学上是同一个东西,加逗号只是让人的眼睛能快速识别量级。因为不涉及数值变化,它的实现可以完全基于字符串操作,做完之后你甚至不需要担心精度问题。

保留两位小数就不一样了,它涉及到数值精度的取舍。1.005保留两位到底是1.00还是1.01,取决于你用哪种舍入规则;1.999进位之后整数部分会不会从1变成2,需要处理进位链;如果是负数,舍入方向又要额外定义。这些都会实实在在地改变数值,改变之后如果再做累加、比较、对账,误差就会暴露出来。

所以一个靠谱的实现思路是:先处理数值,再处理显示。也就是说,先把舍入、进位这些跟数值有关的活儿干完,得到一个确定的小数位数字符串,最后再做千分位分组。顺序反了就容易出问题,比如你先给1,234.5加逗号,再想去补零,中间还得把逗号摘掉,纯属折腾。

1.3 三种实现路线的横向对比

在动手写之前,值得把可选路线摆出来比一比,因为选错路线后面改起来成本很高。我大致归成三类。

实现路线优点主要风险适用场景
正则替换 + toFixed代码短,几行搞定toFixed 浮点舍入不可靠,大数丢精度一次性脚本、演示代码
Intl.NumberFormat / toLocaleString原生能力,自动处理货币符号各环境默认分隔符不统一,输出不可控面向多语言地区的国际化站点
字符串手工解析 + 整数运算结果完全可控,不依赖浮点代码量偏大,需要处理边界金融、财务、对账类业务

我的判断是,只要跟钱真正沾边,第三条路是唯一稳妥的。前两条在日常开发里够用,但只要出现一次对账差错,排查成本远超当初多写的几十行代码。下面我会把第二条路单独讲一节,因为toLocaleString有一个非常隐蔽的坑,很多人到现在还在踩。

2. 核心原理:千分位分组与小数舍入的底层逻辑

2.1 千分位分组的规则到底怎么定义

千分位(thousands separator)的规则说起来一句话:从个位往左数,每三位插入一个分隔符。注意是"从右往左"数,不是从左往右。1234567从右往左切三段是1 | 234 | 567,所以结果是1,234,567;如果从左往右切就变成了123 | 456 | 7,完全错了。

这个方向性决定了正则的写法。我们看一个最常见的写法:

const result = '1234567'.replace(/\B(?=(\d{3})+$)/g, ','); // "1,234,567"

这里的\B是"非单词边界",它保证插入位置不会出现在字符串开头。(?=(\d{3})+$)是一个前瞻断言,意思是"当前位置的右边,需要是 3 的整数倍个数字,并且一直到结尾"。这两个条件一叠加,就恰好落在每一组的边界上。你可以把前瞻理解成一个"不消耗字符的检查",正则引擎看到这个位置满足条件就插逗号,但不移动匹配指针,所以能连续处理多个位置。

这里有个容易忽略的细节:在字符串替换里,如果分隔符本身包含$符号,用字符串形式写替换值会出问题,因为$在 replace 里有特殊含义。稳妥写法是用函数返回:

const group = (intStr, sep) => intStr.replace(/\B(?=(\d{3})+$)/g, () => sep);

另外一点,如果传入的整数部分带了前导零,比如接口返回"0001234",直接分组会变成0,001,234。正确做法是先剥掉前导零,只保留一位:int.replace(/^0+(?=\d)/, '')。

2.2 浮点数为什么必然带来舍入误差

要理解toFixed为什么不可靠,得先接受一个事实:JS 里的数字是 IEEE 754 双精度浮点数,它用 64 位二进制来表示一个数,其中尾数部分只有 52 位有效精度。在十进制里能精确表示的0.1,在二进制里是一个无限循环小数0.0001100110011...,存进去的时候只能截断,于是从存储那一刻起就有了误差。

这就是为什么0.1 + 0.2得到的是0.30000000000000004而不是0.3。同样的道理,1.005在内存里实际存储的值是1.00499999999999989...,你写的是 1.005,机器看到的是比 1.005 小一点点的数。那么(1.005).toFixed(2)得到"1.00"就完全说得通了——它老老实实对那个真实存在的、略小于 1.005 的数做了舍入。

这个问题不能靠"多加一点点"绕过去。有些文章建议写成Number((num + Number.EPSILON).toFixed(2)),这在某些数字上确实能修正,但它是个补丁而不是解决方案,遇到1.335、8.575这类数字照样翻车。真正可靠的做法是避开二进制浮点运算,把小数点后的位数当成字符串来处理,或者干脆在业务层用"分"(整数)来存储金额。

2.3 负数、零值和超大金额的边界规则

边界处理是区分业余实现和专业实现的分水岭。第一个边界是负数:-1234.5应该格式化成-1,234.50,负号在最前面,不参与分组,也不能出现-1,234.-50这种诡异结果。处理办法是先把符号抽出来,只对绝对值做分组,最后再拼回去。

第二个边界是零和极小数:0要显示成0.00;-0在 JS 里String(-0)得到的是"0",所以不用担心出现-0.00;但-0.001保留两位之后是-0.00还是0.00?我的选择是,如果舍入后所有位都是零,就把负号去掉,显示0.00,因为财务上不希望看到"负零"这种东西。

第三个边界是超大金额:9999999999999999(16 个 9)超过了Number.MAX_SAFE_INTEGER(9007199254740991),用 Number 存储会直接丢精度,12345678901234567890变成12345678901234567000。这种量级的金额在真实业务里并不常见,但票据号、流水号经常是这个长度。处理办法是把输入当字符串走字符串运算,或者用BigInt做进位加法。

第四个边界是极小值和非数值:null、undefined、''、NaN、Infinity传进来怎么办?我的习惯是统一返回一个可配置的兜底值,默认"0.00",绝对不能让它抛异常把整个渲染搞崩。

3. 从零手写:三种实现方式的完整代码

3.1 正则替换法的标准写法

先给一个最短可用版本,满足大多数展示场景:

function formatMoneySimple(value, decimals = 2) { const num = Number(value); if (!Number.isFinite(num)) return (0).toFixed(decimals); const fixed = num.toFixed(decimals); // 先定小数位 const [intPart, fracPart] = fixed.split('.'); const grouped = intPart.replace(/\B(?=(\d{3})+$)/g, () => ','); return fracPart ? `${grouped}.${fracPart}` : grouped; } formatMoneySimple(1234567.8); // "1,234,567.80" formatMoneySimple(-1234.5); // "-1,234.50" formatMoneySimple(0); // "0.00"

这个版本的好处是短,坏处也很明显:它把数值舍入完全交给了toFixed,所以formatMoneySimple(1.005)还是会给"1.00"。如果业务上能接受这一点(比如金额来源本来就是两位小数以内),那用它是没问题的。但我建议你至少在注释里标明这个限制,免得半年后有人拿它去算含税价。

另外,split('.')这一步在decimals = 0的时候也要处理:(1234).toFixed(0)得到"1234",没有小数点,fracPart是undefined,所以最后要判断再拼。这个细节比想象中容易漏。

3.2 字符串遍历法的实现思路

如果想彻底摆脱浮点,可以用最朴素的从右往左扫的写法,逻辑清晰,也方便讲解:

function groupByLoop(intStr, sep = ',') { let out = ''; let count = 0; for (let i = intStr.length - 1; i >= 0; i--) { out = intStr[i] + out; count++; if (count % 3 === 0 && i !== 0) { out = sep + out; } } return out; } groupByLoop('1234567'); // "1,234,567" groupByLoop('123'); // "123" groupByLoop('1234'); // "1,234"

这里的关键判断是i !== 0,它保证最后一位(也就是最高位)后面不会多出一个逗号。写循环法的时候我最常见的错误就是漏掉这个条件,结果123456变成,123,456。循环法的性能比正则稍好一点点,但对现代引擎来说差异可以忽略,选哪个主要看团队可读性偏好。我自己的习惯是:展示层用正则,因为短;工具库用循环,因为一眼能看懂边界在哪。

3.3 toLocaleString 的隐藏陷阱

Number.prototype.toLocaleString和Intl.NumberFormat是原生提供的国际化格式化能力,用起来很爽:

(1234567.8).toLocaleString('en-US', { minimumFractionDigits: 2 }); // "1,234,567.80"

但是有两个坑必须知道。第一,默认行为依赖运行环境。不传 locale 的时候,格式化结果取决于运行时的默认区域设置,浏览器、Node、不同版本之间可能不一致,某些环境会用不换行空格(\u00A0)或者窄空格做千分位分隔符,看着像空格又不是空格,做字符串比较会直接失败。所以真要用,一定要显式传'en-US'或'zh-CN'。

第二,它同样受浮点影响。(1.005).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })在不同引擎上的输出并不统一,有的给1.01,有的给1.00,因为它底层用的是Intl的舍入策略,跟toFixed不完全一样。这种不确定性放在跨国财务场景里是很危险的,我宁可自己算。

4. 工程化封装:一份可直接进项目的实现

4.1 入参设计与容错策略

前面三节把零件都准备好了,现在组装成一个能进项目的函数。我的设计目标有四条:一是结果完全确定,同一个输入在任何环境输出一致;二是支持大数字符串,不因Number精度丢失而出错;三是容错,任何奇怪输入都有兜底;四是可配置,分隔符、小数位、前后缀、舍入方式都能改。

配置项我定成这样:

配置项类型默认值说明
decimalsnumber2保留的小数位数,0 表示不带小数
thousandsSepstring","千分位分隔符
decimalSepstring"."小数点符号
prefixstring""前缀,如货币符号
suffixstring""后缀,如"元"
roundModestring"round"round 四舍五入 / floor 舍去 / ceil 进位
fallbackstring"0.00"非法输入时的返回值

roundMode这个参数值得单独说一句。财务上并不是所有场景都用四舍五入,有些企业内部规定"一律舍去"(截尾),有些规定"一律进位"。把它做成配置,比事后改代码要省事得多。

4.2 完整实现代码

function formatMoney(value, options = {}) { const { decimals = 2, thousandsSep = ',', decimalSep = '.', prefix = '', suffix = '', roundMode = 'round', fallback = '0.00' } = options; // ---------- 第一步:归一化输入 ---------- let raw; if (typeof value === 'number') { if (!Number.isFinite(value)) return fallback; raw = String(value); } else if (typeof value === 'string') { raw = value.trim().replace(/[,\s\u00A0]/g, ''); } else if (typeof value === 'bigint') { raw = value.toString(); } else { return fallback; } // ---------- 第二步:结构校验 ---------- const m = /^([+-]?)(\d*)(?:\.(\d*))?$/.exec(raw); if (!m || (!m[2] && !m[3])) return fallback; let sign = m[1] === '-' ? '-' : ''; let intPart = (m[2] || '0').replace(/^0+(?=\d)/, ''); let fracPart = m[3] || ''; // ---------- 第三步:小数位裁剪与进位 ---------- if (decimals > 0) { if (fracPart.length > decimals) { const nextDigit = fracPart.charCodeAt(decimals) - 48; const tail = fracPart.slice(decimals + 1); let carry = false; if (roundMode === 'round') { carry = nextDigit >= 5; } else if (roundMode === 'ceil') { carry = nextDigit > 0 || /[1-9]/.test(tail); } fracPart = fracPart.slice(0, decimals); if (carry) { // 用 BigInt 做加法,彻底避开浮点 const bumped = String(BigInt(fracPart || '0') + 1n).padStart(decimals, '0'); if (bumped.length > decimals) { intPart = String(BigInt(intPart) + 1n); fracPart = bumped.slice(1); // 去掉进位的 1 } else { fracPart = bumped; } } } else { fracPart = fracPart.padEnd(decimals, '0'); } } else { // decimals = 0 时,也需要按第一位小数判断是否进位 if (fracPart.length > 0) { const first = fracPart.charCodeAt(0) - 48; let carry = false; if (roundMode === 'round') carry = first >= 5; else if (roundMode === 'ceil') carry = /[1-9]/.test(fracPart); if (carry) intPart = String(BigInt(intPart) + 1n); } fracPart = ''; } // ---------- 第四步:处理负零 ---------- const isZero = /^0+$/.test(intPart) && (!fracPart || /^0+$/.test(fracPart)); if (isZero) sign = ''; // ---------- 第五步:千分位分组 ---------- const grouped = intPart.replace(/\B(?=(\d{3})+$)/g, () => thousandsSep); // ---------- 第六步:拼接 ---------- const decimal = decimals > 0 ? decimalSep + fracPart : ''; return `${sign}${prefix}${grouped}${decimal}${suffix}`; }

代码不算短,但每一段都能单独解释清楚,维护起来不费劲。我特意用BigInt来做进位加法,这是因为在decimals = 0的场景下,9999999999999999999这种数字用 Number 加 1 会直接变成10000000000000000000,看起来对,但中间过程已经丢过精度了。用 BigInt 就没有这个隐患。

4.3 一组能跑通的用例

写完必须跑一遍,下面这些用例覆盖了我能想到的主要边界:

formatMoney(1234567.8); // "1,234,567.80" formatMoney(-1234.5); // "-1,234.50" formatMoney(0); // "0.00" formatMoney(-0.001); // "0.00"(负零被抹掉) formatMoney(1.005); // "1.01"(字符串解析,不受浮点影响) formatMoney(1.335); // "1.34" formatMoney(9.999); // "10.00"(进位到整数) formatMoney('12345678901234567890'); // "12,345,678,901,234,567,890" formatMoney(1234.5, { decimals: 0 }); // "1,235" formatMoney(1234.4, { decimals: 0, roundMode: 'floor' }); // "1,234" formatMoney(1234.41, { decimals: 0, roundMode: 'ceil' }); // "1,235" formatMoney(1234.5, { prefix: '¥' }); // "¥1,234.50" formatMoney(null); // "0.00" formatMoney('abc'); // "0.00" formatMoney('1,234.5'); // "1,234.50"(已有逗号也能吃进去)

这里最值得说的是formatMoney(1.005)那一行。因为函数把数字先String()成"1.005",再走字符串裁剪,所以它给出的是符合直觉的"1.01"。但我要提醒一句:这个"正确"只在输入本身是精确的1.005时才成立。如果这个 1.005 是前面某次浮点计算的结果,String()拿到的可能已经是"1.0049999999999999",那就进不了位。要彻底解决,必须在数据源头上用整数分存储,格式化只管展示。这一点我在下一节会展开讲。

5. 踩坑记录与常见问题速查

5.1 精度类问题的排查与修复

精度问题是这个主题里最容易翻车的地方,我整理了几类典型情况。

现象根本原因推荐处理
1.005 显示成 1.00浮点存储实际略小于 1.005改字符串解析实现,或数据层用整数分
0.1 + 0.2 参与格式化后不对二进制浮点固有误差金额统一按分(整数)存储和计算
大额数字末几位变 0超过 Number.MAX_SAFE_INTEGER用字符串或 BigInt 传递
累加 1000 笔后有几分偏差每笔都做了浮点舍入只在最终展示时格式化一次

这里我想重点讲最后一条,因为它是最容易被忽略的。很多人为了"每一步都准确",在每一次中间计算之后都调用格式化,或者在每个子项上做toFixed,结果误差被一步步放大。正确的做法是:计算阶段全程保持原始精度(最好用整数分),只在最终输出给人看的那一瞬间做一次格式化。这条规则听起来简单,但在一个十几人协作的项目里,能坚持下来的团队不多。

至于数据层用整数分,具体做法是:数据库里存amount_cents(单位分)这样的整数字段,接口传递也传整数,前端拿到之后除以 100 只是为了展示,运算阶段不做除法。这样从写入到读取到汇总,全程没有精度损失,对账永远不会差钱。如果是多币种,需要注意日元这类没有小数位的货币,用统一除以 100 就不对了,这时候要在币种配置里加一个exponent字段。

5.2 格式化之后还能不能算回去

格式化后的字符串有个特点:它已经不再是数字了。parseFloat('1,234.56')的结果是1,因为解析到逗号就停了;Number('1,234.56')直接给NaN。这两个行为都符合规范,但用户不知道,所以经常会有人把已格式化的字符串塞回计算函数里,得到一堆莫名其妙的结果。

如果你确实需要在某个环节把展示字符串还原成数字(比如用户手动改了输入框里的金额),那就得先清洗再转换:

function parseMoney(input) { if (typeof input === 'number') return input; if (typeof input !== 'string') return NaN; // 去掉空格、逗号、货币符号等一切非数字字符,只留下数字、小数点、正负号 const cleaned = input.replace(/[^\d.+-]/g, ''); if (!cleaned || cleaned === '-' || cleaned === '+') return NaN; if ((cleaned.match(/\./g) || []).length > 1) return NaN; const n = Number(cleaned); return Number.isFinite(n) ? n : NaN; } parseMoney('1,234.56'); // 1234.56 parseMoney('¥ 1,234.56'); // 1234.56 parseMoney('--1'); // NaN parseMoney('1.2.3'); // NaN

这里用includes或者正则去判断有没有多余的小数点,是为了防止用户输入1.2.3这种非法内容。如果你只是想快速判断一个字符串里有没有逗号,'1,234'.includes(',')就够了,但要注意String.prototype.includes是区分大小写的,判断字母的时候别搞错。

还有一个容易被忽略的点:还原之后的数字应该立刻参与计算,不要再显示回输入框,否则会出现"输入 1,234.56 → 还原成 1234.56 → 再格式化回 1,234.56"这种无意义的往返,用户看到框里内容闪来闪去。

5.3 输入框实时格式化的光标问题

这是所有做财务表单的人都要过一次的关。用户在一个已经格式化成1,234.5的输入框里继续打字,你如果在每次input事件里重新格式化成1,234.56,光标默认会跳到最末尾,用户想改中间某一位就得重新点一次,体验极差。

解决思路是在格式化前后记录并恢复光标位置。核心代码大概是这个思路:

function formatWithCursor(inputEl) { const raw = inputEl.value; const caret = inputEl.selectionStart; // 计算光标之前有多少个数字字符 const digitsBefore = raw.slice(0, caret).replace(/\D/g, '').length; const formatted = formatMoney(raw, { decimals: 2 }); inputEl.value = formatted; // 从格式化结果里找到第 digitsBefore 个数字后面的位置 let pos = 0, seen = 0; while (pos < formatted.length && seen < digitsBefore) { if (/\d/.test(formatted[pos])) seen++; pos++; } inputEl.setSelectionRange(pos, pos); }

逻辑不复杂,但一定要记住:只在input事件里做,不要在blur里覆盖用户正在输入的内容。另外还有个更省事的方案,就是输入时不做千分位,只在失去焦点(blur)的时候格式化,聚焦时再还原成纯数字。微信支付、支付宝的转账金额输入框基本都是这么做的,用户感知很好,实现成本也低。我个人更推荐这个方案,因为它的边界情况少得多。

5.4 一份问题速查清单

最后整理一份可以贴在工位上的清单,遇到问题先对着查。

  • 数字末几位变成 0:检查是不是超过安全整数范围,改用字符串传输
  • 显示 1.00 但应该是 1.01:检查数据源是不是浮点计算来的,从源头改成整数分
  • 千分位分隔符看起来像空格:从环境里复制出来用charCodeAt查一下,多半是不换行空格
  • 负数显示成1,234.-50:符号处理顺序错了,先抽符号再分组
  • 输入框光标老是跳末尾:用上面 5.3 的方案,或者改成失焦时格式化
  • 空字符串进去变成0.00而你想要空白:把fallback配置成''
  • 表头显示的金额和小计对不上:检查是不是每行都做了舍入再求和

6. 把格式化能力嵌进真实项目

6.1 列表和数据表格中的批量处理

表格里做金额格式化,性能通常是够用的,因为一个页面几十上百行,每行做一次字符串操作的开销可以忽略。真正需要留意的是渲染时机,如果你的表格支持排序和筛选,排序必须按原始数值排,不能按格式化后的字符串排,否则9.00会排在10.00前面,因为字符串比较是逐位比的。

在 Vue 和 React 里的写法略有不同。Vue 3 移除了过滤器,推荐用计算属性或者方法;React 里直接在渲染时调用函数就行。如果表格列很多,我会把格式化函数包一层useMemo或者缓存,避免每次渲染都重新算一遍。不过说实话,除非数据量真的很大,否则这点优化意义不大,我更愿意把精力放在数据源头的精度治理上。

还有一个实用的小技巧:在表格单元格上加一个title属性,里面放原始数值,鼠标悬停就能看到未格式化的完整数字,这样既不破坏视觉,又方便排查问题。这个小细节在跟财务对接的时候特别受欢迎。

6.2 表单输入的实时限制与校验

金额输入框的规则我一般会限制得比较严:只允许数字和一个小数点,小数点后最多两位,不允许前导的多个零,不允许+号。实现上就是在input事件里做逐层过滤:

function sanitizeMoneyInput(str) { let v = str.replace(/[^\d.]/g, ''); // 只留数字和小数点 v = v.replace(/^\./, '0.'); // 开头是点,补个 0 v = v.replace(/^0+(\d)/, '$1'); // 去掉多余前导零 const parts = v.split('.'); if (parts.length > 2) { v = parts[0] + '.' + parts.slice(1).join(''); // 多个点,合并 } const [i, d] = v.split('.'); return d !== undefined ? `${i}.${d.slice(0, 2)}` : i; }

这套过滤的好处是用户无论怎么乱输,框里始终保持一个合法的中间状态,不需要弹提示打断他。但要注意一个副作用:在用户输入1.的瞬间,这个状态其实是"半成品",你不能立刻在blur之前给他补成1.00,否则他没法继续输小数。所以补零的动作一定要放在失焦时。

6.3 扩展方向:多币种与自定义符号

如果业务会涉及多币种,格式化函数还需要一个币种参数。常见的做法是维护一份币种配置表,里面记录符号、小数位数、符号位置(前置还是后置)。比如美元是前置符号保留两位,日元是前置符号零位小数,欧元在某些地区是后置符号。有了这张表,格式化函数只需要多读一个exponent和symbolPlacement再决定怎么拼。

这里有个反复被踩的坑:不要把货币符号写死在格式化函数里。我见过一个项目,符号直接写在模板字符串里,后来业务要支持东南亚市场,满项目搜索加号在哪,改了一周。把符号、小数位、分隔符全部参数化,成本很低,收益很大。

提示:多币种场景下,金额的存储精度要按最大需求来定。如果系统里既有两位小数的币种也有零位小数的币种,统一按两位小数存整数分是最省事的方案,日元这种在后面展示时把小数位配置成 0 就行。

写完这些之后,我实际项目里的做法是把整个格式化逻辑独立成一个money.js模块,对外只暴露formatMoney和parseMoney两个函数,其余的内部函数都不导出。这样业务代码只用关心"给我一个数,还我一个字符串",改动都收敛在这一个文件里。踩过几次因为舍入规则不统一导致的对账问题之后,我现在养成的习惯是:任何跟金额相关的工具函数,第一件事就是写测试用例,尤其是1.005、9.999、-0.001、超大整数这几个,跑通了再往下写业务。这个习惯帮我省掉了很多返工时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询