1. 数字格式化这件事,为什么要在前端反复做
1.1 从一版报表需求说起
很多前端开发迟早会遇到这样一个需求:页面上要展示商品价格、订单金额、库存数量、手续费比例,数据从后端接口拿过来往往是1234567.891这种原生态数字。如果直接渲染到页面上,不仅丑,而且用户根本看不明白——没有千分位分隔,不知道这是十二万还是百万;小数位数不一致,金额对不上;语言环境不同,数字书写习惯也不一样。
我第一次认真对待这个问题,是在某个模拟项目做运营报表的时候。接口返回一堆订单量和成交额,产品经理要求“金额保留两位小数,每三位一个逗号,百分比要带百分号,负数用括号表示”。当时的第一反应就是正则加一堆字符串拼接,写出来也能跑,但代码越写越心虚:浮点数精度、四舍五入规则、千分位分段、不同地区习惯……每个点都有人在踩坑。
后来我在团队代码里看到了Intl.NumberFormat,了解完才发现,这其实是 JavaScript 原生提供的一套数字格式化 API,早就在浏览器里躺着,只是很多项目里没被真正用好。它解决的不只是“加逗号”这么一件事,而是把数字的多语言、多风格、多单位输出统一管理起来。这篇文章我就把自己实际项目里的使用思路、配置参数、踩坑记录、以及和其他方案对比的结果整理一遍。
1.2 你到底在格式化什么样的数字
聊 API 之前,先划清边界。前端里的数字格式化至少要覆盖这几类场景:
- 金额类:货币符号、千分位、固定小数位,比如
¥ 12,345.67。 - 数量类:大数字千分位,比如
1,234,567,或者带缩写123.4万。 - 比例类:百分比、千分比,比如
12.34%。 - 工程类:科学计数法、有效位数,比如
1.235E6。 - 序号类:两位数字补零之类,不过那个通常用
padStart就够了,不算 Intl 的主场。
这些需求以前大家最常写的是手抄公式:num.toFixed(2)再正则插入逗号;或者用正则/\B(?=(\d{3})+(?!\d))/g去分割整数部分。单个需求没问题,但一旦遇到“保留两位小数但是要四舍五入”“负数要不要带符号”“百万以上显示成 xxx万”“阿拉伯数字和中文数字混排”这些叠加条件,手写方案就变得脆弱。
Intl.NumberFormat最聪明的设计在于:它把“规则”和“执行”分开。你告诉它用什么地区习惯、什么显示风格,它内部帮你处理所有规则细节。这篇我会用一个完整项目里最常碰到的配置清单来拆它的用法,也会把手写正则方案和它的优缺点摆在桌面上对比,方便后续接手的人直接参考。
2. Intl.NumberFormat 的设计意图:为什么它值得当首选
2.1 构造参数拆解:两个参数四种能力
Intl.NumberFormat是个构造函数,基本用法是:
const nf = new Intl.NumberFormat(locales, options);第一个参数locales控制语言区域,比如zh-CN表示中文中国、en-US表示美式英语、de-DE表示德语德国。第二个参数options是整形配置对象,里面常见的组合如下:
const nf = new Intl.NumberFormat('zh-CN', { style: 'currency', // 货币、百分数、普通数字 currency: 'CNY', // 货币代码 minimumFractionDigits: 2, // 最少小数位数 maximumFractionDigits: 2, // 最多小数位数 useGrouping: true, // 是否分组(千分位) notation: 'standard', // 标准计数还是压缩计数 signDisplay: 'auto' // 正负号显示策略 });我在项目里最常挂嘴边的就是四个维度:style决定整体风格、currency决定金额符号、fractionDigits决定小数精度、notation决定计数写法。
为什么把“区域”和“风格”拆成两层?因为数字不是全人类统一的。同一个1234567.8,在印度可能被分成12,34,567.8,在德语地区小数点和小数点逗号刚好反过来。如果自己写正则,你根本不可能为全世界每个地区维护一份分组规则。而Intl内部维护了一套国际化数据,它知道每个区域怎么写数字。你的代码不需要知道规则,只用告诉它“我要给谁看”,这就是它最大的价值。
2.2 几个典型配置组的输出效果对照
我们先把最常用的一组配置贴出来,我建了一个对照表,方便实际开发时直接查。
| 目标场景 | locales | options | 输入数字 | 输出结果 |
|---|---|---|---|---|
| 人民币金额 | zh-CN | {style:'currency', currency:'CNY'} | 1234567.8 | ¥1,234,567.80 |
| 美元金额 | en-US | {style:'currency', currency:'USD'} | 1234567.8 | $1,234,567.80 |
| 欧元金额 | de-DE | {style:'currency', currency:'EUR'} | 1234567.8 | 1.234.567,80 € |
| 百分比 | zh-CN | {style:'percent', maximumFractionDigits:1} | 0.1234 | 12.3% |
| 千分位数字 | en-US | {} | 9876543.21 | 9,876,543.21 |
| 压缩显示 | zh-CN | {notation:'compact', maximumFractionDigits:1} | 1234567 | 123.5万 |
| 科学计数 | en-US | {notation:'scientific'} | 1234567 | 1.234567E6 |
这张表我到现在还会贴在团队文档里。每次产品提格式化需求,先对着表找一行,基本十分钟能确定配置,不用再临时试正则。
有一点要特别提醒:style: 'percent'的时候,输入的值是小数表示法,0.1234 输出12.3%,不是直接把 12.34 变成12.34%。如果你后端直接返回12.34这个数值,你得先除以 100 再交给 formatter,否则输出会变成1,234%。这个错位我自己在图表组件里踩过一次,改了半天才发现问题出在“数值单位理解不一致”。
2.3 为什么 format 方法可以被放心复用
Intl.NumberFormat实例还有一个很实用的特性:它是无状态的。同一份格式化规则,不管 format 多少次,结果都不会因为上次调用而改变。这意味着你可以在模块顶层创建一个全局实例,反复使用,不需要每次循环里都 new 一个。
const priceFormatter = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 2, maximumFractionDigits: 2 }); // 在循环里复用同一个实例 const totals = [1234.5, 5678.9, 9876.1]; const result = totals.map(item => priceFormatter.format(item)); // 输出:["¥1,234.50", "¥5,678.90", "¥9,876.10"]这种复用习惯在小页面里看不出差异,一旦表格要渲染几百行、图表 tooltip 每秒触发几十次,实例复用的收益就非常明显。浏览器内部对Intl的格式化器也有优化缓存,同一个实例的 format 方法执行速度明显比反复构建新实例快。这个点后面我专门测过一组数据,放到性能小节再说。
3. 实操配置明细:从订单页到报表中心
3.1 订单金额与货币符号处理
电商后台的订单列表是最常见的场景。金额显示需要货币符号、千分位、两位小数,同时还要注意“不同币种对应不同符号”。这里我一般会封装一个通用的 createMoneyFormatter 函数:
function createMoneyFormatter(locale, currencyCode, fractionDigits = 2) { return new Intl.NumberFormat(locale, { style: 'currency', currency: currencyCode, minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }); } const cnyFormatter = createMoneyFormatter('zh-CN', 'CNY'); const usdFormatter = createMoneyFormatter('en-US', 'USD'); console.log(cnyFormatter.format(123456.789)); // "¥123,456.79" console.log(usdFormatter.format(123456.789)); // "$123,456.79"注意这里minimumFractionDigits和maximumFractionDigits我是故意设成相同的值,这样金额永远保持两位小数,不会金额一会儿是¥123.00一会儿是¥123。很多表格里的数字对不齐,就是因为没固定小数位。
不同地区对货币符号的位置有自己约定,比如美元符号在金额前,而部分欧洲国家的符号在金额后面。交给Intl.NumberFormat就不用自己写判断。有人会担心“我就是想强制人民币符号显示在数字前面怎么办”,那你直接用zh-CN加CNY,它自然会放在前面;如果你必须在en-US情况下显示人民币,输出的就不是CN¥而是CN¥1,234.56,国际习惯如此,不用强扭。
再提醒一个做导出 Excel 时常遇到的坑:Excel 单元格里如果直接塞Intl格式化后的字符串,导出文件里看到的也是一串字符串,不是可计算的数字。一般做法是导出时传原始数字,同时另带 one 列展示格式化文本;如果只允许一列,就要在导出工具里另行处理显示格式。这个属于业务实现,不是Intl本身的问题,但架不住每次都有同学在联调时踩。
3.2 百分比和千分比的正确姿势
报表里现金流量比例、环比增长、库存周转率这类指标,后端给的通常是小数。直接在页面里乘 100 再加%是常见做法,但很粗糙。用Intl.NumberFormat可以顺带控制小数位和千分位:
const percentFormatter = new Intl.NumberFormat('zh-CN', { style: 'percent', minimumFractionDigits: 1, maximumFractionDigits: 1, signDisplay: 'exceptZero' // 正数也显示加号 }); console.log(percentFormatter.format(0.234)); // "23.4%" console.log(percentFormatter.format(-0.1256)); // "-12.6%" console.log(percentFormatter.format(0.08)); // "+8%"signDisplay是一个很容易被忽略但很实用的配置。默认值是auto,意思是负数显示负号、正数不显示加号。想强调“增长”场景,可以设成exceptZero,正数也会带上+。我做同比分析组件的时候尤其喜欢这个配置,一眼就能分辨增长率是正还是负。注意0本身不会带符号,这符合通用阅读习惯。
百分比还有一个隐藏需求:要不要分组。如果百分比数值本身就很大,比如12345.6%,你希望展示成12,345.6%还是12345.6%?useGrouping在这里起作用,默认是true。这块业务上没标准答案,产品说怎样就怎样,但配置能力得知道。
千分比{ style: 'unit', unit: 'percent' }和百分比不一样,这里不建议混淆。实际业务里很少展示千分比,所以多数项目里直接+ '‰'就行了,不值得为它单独维护一套方案。
3.3 万元单位与 compact 压缩显示
给管理后台做首页大屏时,经常要求“成交量显示成 xx 万”。这时候notation: 'compact'就很好用:
const compactFormatter = new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 1, compactDisplay: 'short' }); console.log(compactFormatter.format(2340000)); // "234万" console.log(compactFormatter.format(2345678)); // "234.6万"compactDisplay有两个候选:short输出234万,long会输出234.6 万之类的长写法。我一般只用short,因为大屏空间宝贵。
在实际项目里,这种格式适合概览卡片和图表坐标轴,但不能用在明细列表里——明细要求精确到个位,压缩显示会丢精度。我见过某次列表里误用了 compact,订单总金额显示成987.7万,看起来没问题,但导出明细时对不上账,排查半天才定位到格式化层。经验教训是:概览用压缩,明细则必须标准。
4. formatToParts 和更细的控制切面
4.1 从字符串到结构化部件
大多数情况下,format()返回一个字符串就够用了。但有些场景需要我们把数字的整数部分、小数部分、货币符号、分组分隔符拆开,自己拼装到不同 DOM 节点或图表文字里。比如大屏上想突出显示整数部分、弱化小数部分。
这时就需要formatToParts()。它把格式化结果拆成一个数组,每一项有type和value两个字段:
const nf = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }); console.log(nf.formatToParts(12345.67)); // 输出近似: // [ // { type: 'currency', value: '¥' }, // { type: 'integer', value: '12' }, // { type: 'group', value: ',' }, // { type: 'integer', value: '345' }, // { type: 'decimal', value: '.' }, // { type: 'fraction', value: '67' } // ]拿到这个结构后,处理逻辑就变得非常直观。你可以只取integer部分拼在一起,也可以把group分隔符替换成空格或窄空格,甚至可以按 type 给不同 DOM 节点加不同样式。
4.2 用 formatToParts 实现分段高亮
某次做图表 tooltip 时,产品要求金额数字里的整数部分加粗、小数部分灰色、货币符号红色。直接拿到字符串去做 split 非常难看,因为你不知道分隔符是什么、货币符号在哪个位置。如果用formatToParts,就变成了一个很简单的数组遍历:
function formatMoneyWithHighlight(amount) { const nf = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }); return nf.formatToParts(amount).map(part => { if (part.type === 'currency') { return `<span class="currency">${part.value}</span>`; } if (part.type === 'fraction') { return `<span class="fraction">${part.value}</span>`; } return `<span class="integer">${part.value}</span>`; }).join(''); }这个方案避免了任何字符串硬解析,因为小数点的位置、负号位置、货币符号位置都已经被Intl规定好了。工具提示框里直接插这段 HTML,样式也可以按类统一控制。要注意的是formatToParts性能略低于format,但只用于 tooltip,每秒触发频率不高,实测没有问题。
4.3 数字在中文环境下的单位别名
用formatToParts还能帮你自动识别单位信息。比如notation: 'compact'模式下,formatToParts()会返回一个type: 'compact'的片段,里面是万、亿这类单位文本。这让“整数 + 单位”样式拆分变得容易。
const compactFormatter = new Intl.NumberFormat('zh-CN', { notation: 'compact', maximumFractionDigits: 1 }); const parts = compactFormatter.formatToParts(1234567); console.log(parts); // [{ type: 'integer', value: '123' }, { type: 'decimal', value: '.' }, { type: 'fraction', value: '5' }, { type: 'compact', value: '万' }]这个结构对接大屏动画特别方便:你可以把数字部分做播报动画,单位部分直接挂到文本末尾。如果用format()拿到整串字符串123.5万,你还需要自己正则去拆,麻烦得多。
5. 那些“原生以外”的旧方案,究竟输在哪里
5.1 手写正则加 toFixed:短平快但藏雷
很多老项目里,数字格式化就一句话:
function formatNumber(num) { return num.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); }这行代码在大部分正数场景下确实能用,比如1234567.8会变成1,234,567.80。但问题藏在边界里:
- 没有负数、零、小数少于两位时的统一显示策略。
- 没有处理超大数或科学计数法表示的数字。
- 没有兼容不同地区小数分隔符差异。
toFixed的结果是字符串,再 replace 时基于的已经是四舍五入后的值,这里没问题;但toFixed本身在某些精度极端情况下会有浮点误差,比如(1.005).toFixed(2)在一些环境里返回1.00而不是1.01。
我还见过一段先乘 100 再除回来自做百分比计算的代码,浮点误差直接把结果弄成14.999999%,前端同学不由自主地写了个兼容写法。这些本质上都是因为把“展示规则”和“数值运算”混在了一起。Intl.NumberFormat的思路是数值保持原样,展示规则由内部统一处理,能避开很多浮点坑。
5.2 第三方工具库:功能强但项目体积吃重
社区里也有像numeral.js、accounting.js、numbro这类专门处理数字格式化的库。早期在某些场景下非常好用,支持链式调用、自定义模式。但现在的 web 项目普遍对包体积敏感,尤其移动端 H5,一个库存几千行、只为加个千分位,性价比确实低。
拿numeral来说,就算只按需引入核心调用,打包体积也会比Intl多出 20KB 左右,而且你还得额外处理它的语言包加载。这不是说不能用,只是它解决的问题如今原生 API 都已经覆盖,没必要再为了格式化数字而引一个库。
有一种情况我会建议继续用第三方库:团队历史代码已经大量依赖某个库,并且很多地方跨模块引用了它的格式化函数。这时候为了阅读一致性和改动成本,维持现状是可以接受的。但如果从零开始一个新项目,我的明确建议是直接上Intl.NumberFormat,删掉一个依赖就是给项目减一次风险。
5.3 多端统一和 Node 端注意事项
前端定方案经常会忽略 Node 端。如果你的项目同时跑着服务端渲染(SSR),模板里也要格式化金额,那Intl在 Node 上完全可用,而且从 Node 14 开始完整支持 ICU 字符集。这就意味着同一个Intl.NumberFormat配置在前端、后端都能用,代码可以抽到公共包里。
我遇到过一种情况:前端页面用Intl格式化,后端导出的 Excel 文件里数字写成1234567原样,两边对不上。后来统一在服务端也封装了一个同样的 formatter,写入导出文件之前先用同一套规则做展示,数据就一致了。所以这里想特别提醒:数字格式化规则最好抽象成一个公共模块,前后端共用,而不是各自单独写一份正则。否则今天前端改了小数位,后端没改,明天报表就千奇百怪。
6. 兼容性排查与降级方案,别把方案锁死在现代浏览器
6.1 支持范围和特性检测
不少同学担心Intl.NumberFormat的兼容性,这里把实际情况说清楚。主流的 Chrome、Edge、Firefox、Safari 从很早的版本就支持基础用法,现在生产环境中几乎不用考虑兼容问题。真正需要注意的场景是部分旧版移动 WebView、低版本安卓系统自带的浏览器、以及个别自定义内核的桌面壳。
建议在入口做一次轻量特性检测:
const hasIntlNumberFormat = typeof Intl !== 'undefined' && typeof Intl.NumberFormat === 'function' && typeof Intl.NumberFormat.prototype.formatToParts === 'function';如果连formatToParts都需要检测,说明要用到分段控制;如果只是format(),也可以只检测format。对于不支持的环境,常见的做法是打一个补齐旧机制的Intlpolyfill。不过要注意,NumberFormat的完整 polyfill 体积不小,它会用 JavaScript 模拟 locale 数据,包体积有些大。真要接的话,建议只在检测失败时动态加载:
if (!hasIntlNumberFormat) { // 动态加载 polyfill await import('intl-numberformat-polyfill'); }配合动态 import 做按需加载,既不影响现代浏览器性能,也能给老设备留退路。
6.2 轻量降级方案:兜底一个 toFixed
如果你不想引入任何 polyfill 依赖,也可以写一个极轻的降级函数,只覆盖format能力。反正老环境里一般也是“能加个千分位就行”,不是非要支持全套语言差异。
function safeFormatNumber(num, locale = 'zh-CN') { if (hasIntlNumberFormat) { return new Intl.NumberFormat(locale).format(num); } // 简易降级:千分位 + 固定两位小数 const fixed = Number(num).toFixed(2); const parts = fixed.split('.'); parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ','); return parts.join('.'); }这个兜底方案能抓到绝大多数显示需求。但注意它不会正确处理货币符号和单位,所以如果业务强依赖特定货币显示,那还是老老实实接 polyfill,或者在降级分支里补符号配置。
6.3 单元测试:把格式规则钉死在用例里
格式化函数最怕“默默被改坏”。我现在的习惯是给常见配置都写一组快照式断言,至少覆盖以下典型值:
const cases = [ [1234567.8, '¥1,234,567.80'], [0, '¥0.00'], [-1234.56, '-¥1,234.56'], [100, '¥100.00'] ];为什么要把0单独拿出来测?因为0和负数的显示在Intl里都容易有意外,比如某些 region 下负数会用括号显示而非负号,产品可能压根不知道自己的 locale 会显示成(¥1,234.56)。单元测试能逼着你确认规则。我在某国际业务项目里就遇到过:用en-US格式化负数金额,输出是-$1,234.56,但当地财务表达习惯是($1,234.56)。解决办法很简单,配置里加currencySign: 'accounting',输出就会变成($1,234.56)。这个配置藏在角落,文档如果不逐项过一遍根本发现不了。
7. 常见问题速查与避坑指南
7.1 六类高频问题对照表
这几年下来,我整理了一份问题对照表,基本覆盖了群里被问过最多的情况:
| 问题现象 | 产生原因 | 解决方式 |
|---|---|---|
| 百分比输出多了好多位小数 | 忘了设maximumFractionDigits | options 里显式配置小数位 |
25%输出成立25或者漏百分号 | 把 25 当 0.25 传入 | 确认输入值是小数表示,如 0.25 |
负数金额显示为-¥1,234.56而非括号 | 默认currencySign是标准样式 | 设置currencySign: 'accounting' |
数字一大就显示成1.235E6 | 默认notation的标准输出和手写需求不符 | 显式设置notation: 'standard'或compact |
| 表格里金额一会儿三位小数一会儿两位 | minimum和maximum不一致 | 两者设成同一值 |
| 修改 locale 后格式没变 | 实例已创建,locale 不会自动更新 | 换 locale 时要重新创建实例 |
这些都是真实踩过的,不是从文档里抄出来的。特别是最后一条,我见过有人在多语言切换页面里只切了lang属性,但 formatter 实例还是语言切换前的,导致切了英文界面数字却仍然按中文格式显示。正确做法是把 formatter 封装成一个可重建的工厂函数,切换语言时重新生成实例,再触发视图更新。
7.2 一个被忽略的性能细节:实例复用
前面提到过复用实例,这里我给个具体对比。我在项目里循环渲染 2000 行表格数据,分别测了两种写法的耗时:
// 低效:循环里不断创建实例 const start = performance.now(); for (let i = 0; i < 2000; i++) { const nf = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }); nf.format(data[i]); } console.log(performance.now() - start); // 高效:复用同一个实例 const nf = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }); const start2 = performance.now(); for (let i = 0; i < 2000; i++) { nf.format(data[i]); } console.log(performance.now() - start2);在我测的浏览器版本里,第二种写法明显有优势,差值大约在 30% 到 60% 之间。虽然 2000 行也就差个十几毫秒,但页面里同时有表格、图表 tooltip、导出预览时,这个差距会被放大。所以我的代码习惯是:页面级或组件级创建 formatter 实例,能放模块顶层就放模块顶层。
7.3 local 参数到底要不要传全
很多人写new Intl.NumberFormat()不带参数,这在浏览器里会默认使用运行环境的当前语言,比如中文系统就显示中文规则,英文系统就显示英文规则。如果你本来就希望数字跟随系统语言,那没问题。但业务报表通常要固定风格,比如中文站点无论用户在什么环境,金额始终显示成¥1,234.56,此时就必须显式传zh-CN,否则同一个页面在不同浏览器里显示效果不一致。
另一个细节是locale参数的写法。zh-CN和zh都能用,但效果可能有细微差别。真要严格管控,就统一写zh-CN、en-US这种完整格式,别写zh,不然小数分隔符等细节可能落在非预期规则上。
8. 实际项目中我最终采用的封装结构
8.1 一个可落地的 formatter 工厂
基于上面的实操,我在多个项目里沉淀下来的封装结构长这样:
// format/index.js const DEFAULT_OPTIONS = { style: 'decimal', useGrouping: true, minimumFractionDigits: 0, maximumFractionDigits: 3 }; function createFormatter(locales, options = {}) { return new Intl.NumberFormat(locales, { ...DEFAULT_OPTIONS, ...options }); } // 按用途暴露特定实例 export const currencyFormatter = (currency = 'CNY', locales = 'zh-CN') => createFormatter(locales, { style: 'currency', currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }); export const percentFormatter = (locales = 'zh-CN', fractionDigits = 2) => createFormatter(locales, { style: 'percent', minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits }); export const compactFormatter = (locales = 'zh-CN', fractionDigits = 1) => createFormatter(locales, { notation: 'compact', maximumFractionDigits: fractionDigits });调用侧会非常清晰:
import { currencyFormatter, compactFormatter } from '@/utils/format'; const cnyMoney = currencyFormatter('CNY', 'zh-CN'); const compactNum = compactFormatter('zh-CN', 1); cnyMoney.format(1234567.8); // "¥1,234,567.80" compactNum.format(1234567); // "123.5万"封装的价值在于统一配置入口。后续如果要全局调整小数位、切换货币符号风格,只需改动工厂函数,不用满项目找toFixed和正则。
8.2 React 和 Vue 里怎么接入布局
在组件里用,我通常直接在模块顶层创建实例,避免每次 render 都创建:
// React 示例 const priceFormatter = new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY', minimumFractionDigits: 2, maximumFractionDigits: 2 }); function PriceCell({ value }) { return <span>{priceFormatter.format(value)}</span>; }如果组件依赖主题或语言切换,那就把 formatter 实例放在一个useMemo或计算属性里,依赖项设为locale和currency。这样语言切换时只重建这个实例,而不是整个组件重算逻辑。
Vue 3 里同理,若locale来自全局状态,可以用computed返回一个 formatter:
import { computed } from 'vue'; const priceFormatter = computed( () => new Intl.NumberFormat(locale.value, { style: 'currency', currency: 'CNY' }) );这里算的是一个重新生成 formatter 的策略,本身开销不大,但比在模板里每次调用方法内 new 要克制得多。
8.3 表格排序和数值比较的警告
最后必须强调一点:Intl.NumberFormat处理的是“展示”,绝不能用它的输出值去参与排序、比较或运算。有些表格组件用字符串排序,格式化后的¥1,234.56在字典序里就会排在¥999.99前面,因为1小于9,但数值上 1234 明明更大。所以排序字段一定要用原始数值,格式化只在最终渲染层做。
这其实也是整篇博文最想强调的核心:格式化永远是最后一公里。数据在传输、运算、比较、入库时都要保持数值形态,只在展示前转换成对应格式。很多人一上来就想着把格式化后的字符串回填给某个字段,后面越写越别扭。分清“存储值”和“显示值”,很多坑自然就绕开了。
按照我个人几年的实操感受,Intl.NumberFormat是把那些“看起来很小、不做又不行”的数字杂活收拢成一套标准流程的最佳工具。现在新项目要是没有特殊理由,我不会再手写金额格式化的正则了。如果你刚接触它,建议先照着上面的表格把常用配置跑一遍,再做一版自己的工厂函数,项目里该有的能力基本就齐了。