ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端数字格式化实战:Intl.NumberFormat 完全指南

前端数字格式化实战:Intl.NumberFormat 完全指南

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 几个典型配置组的输出效果对照

我们先把最常用的一组配置贴出来,我建了一个对照表,方便实际开发时直接查。

目标场景localesoptions输入数字输出结果
人民币金额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.81.234.567,80 €
百分比zh-CN{style:'percent', maximumFractionDigits:1}0.123412.3%
千分位数字en-US{}9876543.219,876,543.21
压缩显示zh-CN{notation:'compact', maximumFractionDigits:1}1234567123.5万
科学计数en-US{notation:'scientific'}12345671.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 六类高频问题对照表

这几年下来,我整理了一份问题对照表,基本覆盖了群里被问过最多的情况:

问题现象产生原因解决方式
百分比输出多了好多位小数忘了设maximumFractionDigitsoptions 里显式配置小数位
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是把那些“看起来很小、不做又不行”的数字杂活收拢成一套标准流程的最佳工具。现在新项目要是没有特殊理由,我不会再手写金额格式化的正则了。如果你刚接触它,建议先照着上面的表格把常用配置跑一遍,再做一版自己的工厂函数,项目里该有的能力基本就齐了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进