JavaScript toLocaleString 实战:数字、日期格式化与本地化技巧

发布时间:2026/10/19 2:29:30
JavaScript toLocaleString 实战:数字、日期格式化与本地化技巧 不用复杂技巧一个藏在所有对象原型链上的toLocaleString就能把数字、日期、数组的展示问题收拾得干干净净。这篇就把这个函数的常用场景、背后的运行机制、参数怎么配、哪些地方容易翻车一次讲透。1. 为什么值得单独讲 toLocaleString1.1 它解决的痛点格式化不是拼字符串做前端时间久了你会发现代码里最丑的部分往往不是业务逻辑而是那些专门用来“把数据变成人能看的字符串”的代码。比如给数字加千分位有人直接写正则把时间戳变成“2024-01-02 15:30”有人一层层 getFullYear、getMonth、getDate 去拼接中间还要小心翼翼处理补零。这些代码写的时候爽维护的时候全是坑。toLocaleString 的存在就是把这些“格式化”需求收归到一个规范化的 API 里。它不需要你关心当前运行环境是什么语言也不需要你自己去处理负数、小数点、时分秒补零这些边界情况。你只要告诉它“我想要什么样的展示效果”它自己会去匹配对应的规则。这么说吧它实际上是一个本地化格式化入口所有内置对象里基本都能看到它的身影。我第一次意识到这个函数的价值是做几个数据类页面。一个页面里要展示金额、百分比、日期、访问量缩写如果全部手写格式化少说几十行工具函数而且换一套语言环境就要重写一遍。改用 toLocaleString 之后大部分格式化的活都被它接走了代码反而干净很多。给你看一组直观对比。手写千分位你得先处理整数和小数分离还得考虑负数function manualGroup(n) { const [int , dec ] Math.abs(n).toString().split(.); const grouped int.replace(/\B(?(\d{3})(?!\d))/g, ,); return (n 0 ? - : ) grouped (dec ? . dec : ); } const useNative (n) n.toLocaleString(en-US);手写方案在正数整数上勉强能用但一旦遇到负数、小数、极大数、或者要输出货币符号就很麻烦。而 toLocaleString 把这些边界情况全部内置处理掉了。这就是它值得单独讲的原因它不是一个花哨的 API而是一个能实实在在减少重复代码、减少 bug 的底层能力。1.2 一个 API 三种对象很多人以为 toLocaleString 只是 Number 的方法实际上它定义在 Object 的原型链上不同的内置类型对它做了各自的实现。日常开发里接触最多的有三个调用对象作用简单示例Number格式化数字支持小数位、千分位、百分比、货币、单位等(12345.678).toLocaleString(zh-CN, { style: currency, currency: CNY })Date格式化日期时间支持年月日、星期、时区、上下午等new Date().toLocaleString(zh-CN, { dateStyle: full })Array将数组每个元素分别格式化后用逗号连接成字符串[1, 2500, 3600000].toLocaleString(zh-CN)至于 Object 本身它的 toLocaleString 直接走的是 toString 的逻辑所以普通对象调用它一般得到的是[object Object]没有太多实践价值。真正要关注的就是上面三个。数组那个实现容易被忽略但实际上挺有意思它对数组里的每个元素依次调用各自的 toLocaleString再把结果用逗号连起来。也就是说数组里放数字它会逐项格式化数组里放 Date它也会逐项格式化日期。这在做日志拼接、列表展示的时候能帮你少写一个 map。2. 核心机制与参数体系2.1 函数签名与参数含义toLocaleString 的标准签名是value.toLocaleString(locales, options)其中locales是语言标签可以传一个字符串也可以传一个字符串数组。比如zh-CN、en-US、[zh-CN, en-US]都合法。传数组时表示“首选中文如果没有对应的本地化数据就回退到英文”的优先级列表。这个参数经常被人忽略因为它可以不传但不传的时候结果完全依赖运行环境的默认语言不同用户看到的结果可能不一样。options是一个配置对象不同对象类型的配置项不一样。Number 场景关注style、currency、minimumFractionDigits、maximumFractionDigits、notation、useGrouping这些Date 场景关注dateStyle、timeStyle、year、month、day、weekday、hour、minute、second、timeZone这些。理解到这一层调用逻辑就很清晰了先决定是格式化数字还是日期再决定要哪些字段、要不要分组、要不要货币符号。有一点需要注意locales参数在传undefined和完全不传是等价行为但如果你只传了options而把locales省掉有些旧环境可能会报错或行为不一致。我的习惯是永远把locales显式传上哪怕你自己觉得“默认语言就行”也要写清楚避免跨环境表现漂移。2.2 底层是 Intl 家族toLocaleString 并不是什么黑魔法它背后是一整套Intl国际化标准的封装。简单说Number.prototype.toLocaleString等价于Intl.NumberFormat.prototype.formatDate.prototype.toLocaleString等价于Intl.DateTimeFormat.prototype.format。这意味着两件事。第一如果你要在一个大循环里格式化几百上千条数据直接调Intl.NumberFormat实例会比反复调toLocaleString性能好很多因为格式化的规则解析可以只做一次。第二如果你想研究 toLocaleString 的某个参数到底生成了什么格式完全可以去查Intl.NumberFormat和Intl.DateTimeFormat的文档理论上它们是一套规则。我自己项目中遇到复杂格式化需求时会更倾向直接用Intl对象而不是 toLocaleString。举个例子const formatter new Intl.NumberFormat(zh-CN, { style: currency, currency: CNY, minimumFractionDigits: 2, }); // 循环里直接复用 for (const item of rows) { console.log(formatter.format(item.amount)); }这段代码比在循环里不断调用item.amount.toLocaleString(...)要高效而且语义更明确。日常一次性格式化用 toLocaleString 完全够了但凡是性能敏感的批量场景记得切换到 Intl 实例。2.3 locales 与 options 如何影响输出语言标签看起来简单实际上有一套回退逻辑。你传zh-CN引擎会先找简体中文的规则找不到就找zh再找不到就按引擎内置的默认区域走。传数组的作用就是指定回退顺序。options 的配置项虽然多但理解几个最常用的就够了。拿 Number 来说useGrouping控制要不要千分位分隔符minimumFractionDigits和maximumFractionDigits控制小数位范围style切换普通数字、百分数、货币、单位等模式。拿 Date 来说dateStyle: full会输出“2024年1月2日 星期二”这种长格式timeStyle: short会输出“下午3:05”这种短格式组合使用就能覆盖绝大多数报表场景。还有一个不少人忽略的配置numberingSystem。这个参数可以切换数字符号系统比如hanidec可以把 “123” 显示成 “一二三”arab可以显示成阿拉伯-印度数字。在某些业务里需要中文数字展示时这个参数能直接解决问题不用自己写映射表。3. 常用场景逐一拆解3.1 数字场景从千分位到货币数字格式化是 toLocaleString 最常被用到的场景。最基础的就是千分位const n 1234567.891; n.toLocaleString(en-US); // 1,234,567.891 n.toLocaleString(zh-CN); // 1,234,567.891这里能明显看到en-US和zh-CN在千分位规则上是一致的如果不传 locales在非中文环境下结果就可能不一样。所以我在项目里通常固定传zh-CN保证所有用户看到同样的格式。如果你要的是国际通用格式也可以固定en-US。数字场景的进阶玩法是控制小数位。这里有个容易踩坑的细节minimumFractionDigits和maximumFractionDigits是配合使用的。比如保留两位小数同时不足两位补零可以这样写(1234.5).toLocaleString(zh-CN, { minimumFractionDigits: 2, maximumFractionDigits: 2, }); // 1,234.50如果只写maximumFractionDigits: 2那1234.5会输出1,234.5因为下限还是默认的 0。很多人在这个细节上栽过跟头以为设置了最大值就会自动补零实际上补零要靠最小值。货币是另一个高频场景。style: currency加上currency参数就能输出带货币符号的字符串而且符号位置、空格规则都由引擎按区域处理(12345.678).toLocaleString(zh-CN, { style: currency, currency: CNY, currencyDisplay: symbol, }); // ¥12,345.68 (12345.678).toLocaleString(en-US, { style: currency, currency: USD, currencyDisplay: name, }); // 12,345.68 US dollarscurrencyDisplay有三个可选值symbol输出符号code输出货币代码name输出货币名称。具体用哪个取决于你的页面是简洁展示还是需要给用户更多上下文。百分比的场景也很常见。style: percent会自动把数值乘以 100 再带上百分号所以传 0.1234 表示 12.34%(0.1234).toLocaleString(zh-CN, { style: percent, minimumFractionDigits: 2, maximumFractionDigits: 2, }); // 12.34%我见过很多人写百分比是自己在数据层乘 100 再拼一个%然后小数位又在展示层处理一遍两处逻辑重复还容易不一致。直接用style: percent数据层保持原始比例展示层负责格式化逻辑反而干净。还有一个很实用的notation配置适合做大数字缩写。比如访问量 123456789要展示成“1.23亿”这种紧凑形式(123456789).toLocaleString(zh-CN, { notation: compact, maximumFractionDigits: 2, }); // 1.23亿 (123456789).toLocaleString(en-US, { notation: compact, maximumFractionDigits: 2, }); // 123.46M这个特性在统计面板、仪表盘里特别好用。传统做法是手写一个“万、亿”的判断函数还得处理不同语言的单位差异notation: compact直接把这些都封装了。3.2 日期时间场景报表里的时间格式化日期时间格式化是 toLocaleString 的第二大战场。最基础用法是把 Date 对象转成适合展示的字符串const d new Date(2024-01-02T15:05:30); d.toLocaleString(zh-CN); // 2024/1/2 15:05:30不过更常用的是通过dateStyle和timeStyle搭配出不同精度的格式d.toLocaleString(zh-CN, { dateStyle: full }); // 2024年1月2日星期二 d.toLocaleString(zh-CN, { dateStyle: long, timeStyle: short }); // 2024年1月2日 15:05如果你需要更细的控制可以单独设year、month、day、weekday、hour、minute、second、timeZoneName等字段。例如要展示“2024年1月2日 星期二 下午3:05”这种完整格式d.toLocaleString(zh-CN, { year: numeric, month: long, day: numeric, weekday: long, hour: numeric, minute: numeric, hour12: true, }); // 2024年1月2日星期二 下午3:05这里hour12: true控制是用“下午3:05”还是“15:05”在中文环境下输出非常自然不用自己判断上午下午。日期场景里还有一个经常被忽略的需求时区展示。如果业务里涉及不同地区的用户或者展示其他时区的时间timeZone参数能直接指定区域。比如把同一个时间点展示成纽约时间d.toLocaleString(zh-CN, { timeZone: America/New_York, dateStyle: full, timeStyle: short, });这个功能在处理多时区协作系统、跨境业务数据时很实用。传统的做法是手动加减时差然后还要考虑夏令时非常容易错。交给timeZone参数之后引擎自己会处理夏令时和时区偏移。3.3 数组与集合场景列表展示的隐藏能力数组的 toLocaleString 大多数人不怎么用但它其实是“批量格式化”的便捷入口。它会对数组里的每个元素调用元素自身的 toLocaleString再把结果用逗号连接起来。比如[1000, 2500000, 36.5].toLocaleString(zh-CN); // 1,000,2,500,000,36.5初看这个输出会觉得有点怪因为它把每个数字都加了千分位再用逗号连接导致结果里逗号很多。但换个场景就有用了。比如你想把一个时间列表直接拼成字符串记日志数组里放 Date 对象const dates [ new Date(2024-01-02), new Date(2024-01-03), ]; dates.toLocaleString(zh-CN, { dateStyle: short }); // 2024/1/2,2024/1/3数组里每个 Date 会统一按照配置格式化最后输出一个以逗号分隔的字符串。如果你正需要这种批量拼接效果就能省掉一个map().join()。不过要注意数组的 toLocaleString 和普通数组拼接在行为上有差异容易引发预期之外的千分位干扰。如果你只是想把数组转成字符串用join()会更可控只有当你想让每个元素都被格式化时才用数组的 toLocaleString。3.4 业务场景打包一个数据面板里的组合用法把上面这些场景组合起来你可以画出真实业务的样子。假设我正在做一套数据面板需要展示这几个指标今日销售额、订单同比增长率、最近一次出单时间、累计访问量。传统写法要写好几个工具函数用 toLocaleString 组合起来就是一段整洁代码function formatSummary() { const todaySales 123456.7; const growth 0.0536; const lastOrderTime new Date(2024-06-18T14:32:00); const totalVisits 123456789; return { sales: todaySales.toLocaleString(zh-CN, { style: currency, currency: CNY, minimumFractionDigits: 2, }), growth: growth.toLocaleString(zh-CN, { style: percent, maximumFractionDigits: 1, }), lastOrder: lastOrderTime.toLocaleString(zh-CN, { dateStyle: medium, timeStyle: short, }), visits: totalVisits.toLocaleString(zh-CN, { notation: compact, maximumFractionDigits: 2, }), }; }这段代码里金额输出是“¥123,456.70”增长率是“5.4%”时间输出是“2024年6月18日 14:32”访问量是“1.23亿”。每一个指标都是标准的展示格式而且逻辑一眼能看懂。这就是 toLocaleString 真正的价值它把“格式化”这个横切关注点从你的业务代码里剥了出来。4. 实操建议与参数速查4.1 高频参数配置一览我把日常开发中最容易用到的配置整理成了速查表照着抄就行。场景推荐写法示例输出千分位n.toLocaleString(zh-CN)1,234,567.891保留两位补零n.toLocaleString(zh-CN, { minimumFractionDigits: 2, maximumFractionDigits: 2 })1,234.50货币n.toLocaleString(zh-CN, { style: currency, currency: CNY })¥1,234.50百分比n.toLocaleString(zh-CN, { style: percent, maximumFractionDigits: 2 })12.34%紧凑数字n.toLocaleString(zh-CN, { notation: compact, maximumFractionDigits: 2 })1.23亿长日期d.toLocaleString(zh-CN, { dateStyle: full })2024年1月2日星期二短日期加时间d.toLocaleString(zh-CN, { dateStyle: short, timeStyle: short })2024/1/2 15:05指定时区d.toLocaleString(zh-CN, { timeZone: Asia/Shanghai, dateStyle: full, timeStyle: short })2024年1月2日 15:05表格里每一行的配置都是我在真实项目里用过的特别是“保留两位补零”那条容易忘minimumFractionDigits实际输出出来没有补零才知道漏了。4.2 常用工具函数封装为了方便复用我习惯把这些格式化逻辑封装成几个小工具函数。这里直接给出一套我在项目中使用的封装思路function formatNumber(value, options {}) { return Number(value).toLocaleString(zh-CN, { maximumFractionDigits: 2, ...options, }); } function formatMoney(value, currency CNY) { return Number(value).toLocaleString(zh-CN, { style: currency, currency, minimumFractionDigits: 2, }); } function formatPercent(value, digits 2) { return Number(value).toLocaleString(zh-CN, { style: percent, minimumFractionDigits: digits, maximumFractionDigits: digits, }); } function formatCompact(value) { return Number(value).toLocaleString(zh-CN, { notation: compact, maximumFractionDigits: 2, }); } function formatDateTime(value, options {}) { const date value instanceof Date ? value : new Date(value); return date.toLocaleString(zh-CN, { dateStyle: medium, timeStyle: short, ...options, }); }几点心得。第一尽量在封装内部统一zh-CN让全站的展示风格一致。如果未来有国际化需求只需要在封装层改成外部传入的语言标签业务代码不用大改。第二所有函数入口都做一层Number(value)或new Date(value)的转换这样外部传字符串、时间戳、Date 对象都可以容错性更强。第三把最常用的默认值写在封装里业务方不需要每次重复传一长串 options。4.3 性能和复用toLocaleString 虽然方便但如果在一个大循环里反复调用性能是不如复用Intl实例的。我对一万条数据做过简单对比循环里直接调 toLocaleString 明显比复用Intl.NumberFormat慢数量越大差距越明显。原因在于每次调 toLocaleString引擎都要重新解析一遍语言标签和 options而Intl.NumberFormat实例只需要在创建时解析一次后面直接 format 即可。所以这里有一个明确的取舍规则// 数据量小几十条内用 toLocaleString代码简洁 const smallList items.map((item) item.price.toLocaleString(zh-CN, { style: currency, currency: CNY, })); // 数据量大几百上千条复用 Intl 实例 const formatter new Intl.NumberFormat(zh-CN, { style: currency, currency: CNY, }); const bigList items.map((item) formatter.format(item.price));还有一个容易被忽略的点如果你的运行环境很老旧或者嵌在一些特殊的 WebView 里Intl可能支持不完整。这种情况建议引入 Intl 的 polyfill或者至少做一个特性检测。不过对现代浏览器和较新的 Node 来说原生支持已经非常完整正常使用问题不大。5. 常见问题与排查实录5.1 我踩过的几个坑这个函数看起来简单真正用起来坑不少。我把这些年踩过的、以及在各种项目里见过的坑集中整理一下。第一个坑是不传 locales 导致结果漂移。同一段代码在中文环境电脑上跑是“2024/1/2”在英文环境电脑上跑就变成“1/2/2024”甚至数字千分位分隔符都可能不一样。这不是 bug而是 toLocaleString 的“正确”行为因为不传 locales 就是用系统默认语言。但如果你做的是一个固定展示风格的页面不在用户系统里跑就可能出现“开发机正常、用户机器格式错乱”的情况。解决办法就是永远显式传zh-CN或en-US。第二个坑是小数补零失效。想保留两位小数只写了maximumFractionDigits: 2结果1234.5输出1,234.5而不是1,234.50。问题在于补零靠的是minimumFractionDigits不是maximumFractionDigits。这两个参数一个管“最少几位小数”一个管“最多几位小数”要同时设置才会达到保留且补零的效果。第三个坑是日期结果不能反向解析。有人会把格式化后的日期字符串再传给new Date()去做日期运算这是非常危险的做法。比如new Date(2024/1/2 15:05:30)在部分引擎里还能解析但如果你用dateStyle: full输出的是“2024年1月2日星期二”再传给new Date()大概率得到Invalid Date。格式化输出是给人看的不是给代码解析的。你真正要做日期运算应该始终持有原始的 Date 对象或时间戳。第四个坑是数组的逗号干扰。[10000, 20000].toLocaleString(zh-CN)输出是10,000,20,000这里的逗号既包括数组元素分隔符又包括每个数字里的千分位分隔符肉眼几乎无法区分。如果你原本只是想把数组转成字符串千万别用这个方法用join()更直白。第五个坑是选项冲突导致意外结果。例如同时设置了style: percent和notation: compact不同引擎对组合的处理可能不一致。我在一个项目中就遇到过某些环境能正常输出“12.34%”某些环境直接忽略 notation。遇到这种组合需求建议先在小范围做一个兼容性测试或者干脆拆成两步手动拼接。5.2 排查思路与速查表当你发现 toLocaleString 输出和预期不一致时先不要怀疑函数有问题按下面这个顺序排查大多数问题几分钟就能定位。现象可能原因处理方式中英文环境结果不一致没有显式传 locales统一传zh-CN或en-US小数没有按预期补零只设置了 maximumFractionDigits同时设置 minimumFractionDigits货币符号或格式不对currency 传错或 locale 不匹配检查 currency 和 locales 的组合日期显示成英文或非预期格式dateStyle/timeStyle 与 locales 不匹配确认 locales 和 style 搭配在某个浏览器或低版本环境报错环境缺少 Intl 支持引入 polyfill 或做特性检测输出带有奇怪的数字符号numberingSystem 被其他设置覆盖显式指定 numberingSystem 或移除冲突配置这套排查顺序在我实际工作中非常有效尤其“先查 locales 再查 options 组合”这个顺序能避开很多表面上的神秘问题。5.3 备选方案与扩展toLocaleString 并不是唯一的答案有些场景用它反而不合适。比如你要生成本地化味道很轻、机器可读的格式像YYYY-MM-DD HH:mm:ss用它反而要传一堆参数结果还未必精确。这种固定格式需求用原生getFullYear那套组合拼字符串或者Date的toISOString().slice()都更直接。再比如你要做更复杂的日期计算比如“三天后”“下周一”这种相对时间toLocaleString 就不适合了应该用相对时间格式化工具。toLocaleString 的定位始终是“把已有的时间点或数字按照区域规则转成展示字符串”它不做日期运算也不做字符串转日期。理解好这个边界你才不会在错误场景里硬套它。还有一个扩展方向如果你需要给自己的对象自定义 toLocaleString完全可以重写这个方法。比如一个商品对象你希望它被数组 toLocaleString 调用时变成有格式的金额就可以给对象定义一个toLocaleString方法。内部把商品价格做成格式化输出这样数组的批量拼接也能一并生效。这个能力挺适合做展示层模型我实际用下来很顺手。最后分享一点实际开发中的体会toLocaleString 不属于那种“雪中送炭”级的功能但只要你持续在做数据展示就会慢慢发现它的价值。我自己最深的感受是它能帮你砍掉一大批“为了展示而存在”的工具函数还能顺带解决本地化差异。唯一要记住的原则是给用户看的东西才用 toLocaleString给机器读的数据千万别用它。如果你在格式化结果上遇到了诡异问题先别改代码检查运行环境和 locales。这两个因素决定了它输出结果的“默认值”出问题大概率就在那里。之后如果遇到更复杂的业务场景比如你想让整站格式风格完全统一再往Intl.NumberFormat实例复用的方向走性能和一致性都会再上一个台阶。