前端知库

怎样给数字加千分位分隔符?

业务里直接用 Intl.NumberFormat 或 toLocaleString,它会按地区规则加分隔符、处理小数和货币。面试手写一般用正则 /\B(?=(\d{3})+(?!\d))/g 只处理整数部分,或者从右往左每三位切一段;要注意负号、小数部分不能被分隔,以及超出安全整数的数字要以字符串处理。

参考答案

简答

订单金额 1234567.89 要显示成 1,234,567.89,实际项目里最好直接用浏览器内置的 Intl.NumberFormat,或者 num.toLocaleString('en-US')。它们按地区规则分组,还能顺便控制小数位、加货币符号,不同语言环境下分隔符也会变,比如德语用点号分组、逗号做小数点。

面试让手写,常见有两种思路。一种是正则:先把数字拆成整数和小数两部分,只对整数部分执行 replace(/\B(?=(\d{3})+(?!\d))/g, ',')。意思是在这样的位置插入逗号:它不是单词边界,也就是前面还有数字,而且它后面跟着若干组正好三位的数字,一直到整数部分结束。另一种是循环:从整数部分的末尾往前,每三位截一段,最后用逗号拼起来。

不管哪种写法,都要先处理三个边界:负号要先拿掉,最后再加回去;小数部分不能被分组,0.12345 的小数不能变成 0.12,345;超过 Number.MAX_SAFE_INTEGER 的数字传进来时可能已经丢了精度,要以字符串的形式处理。

要注意,toLocaleString() 不传参数时用的是用户系统的地区设置,不同用户看到的格式可能不一样,需要固定格式时要显式传地区。

用 Intl.NumberFormat

JavaScript
const format = new Intl.NumberFormat('en-US', { maximumFractionDigits: 2 });
console.log(format.format(1234567.891)); // 1,234,567.89
console.log(new Intl.NumberFormat('de-DE').format(1234567.5)); // 1.234.567,5
console.log(new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(1234.5)); // ¥1,234.50

为什么德语环境输出的是 1.234.567,5?因为分组和小数点用什么符号,本来就是地区习惯。Intl.NumberFormat 内置了这些规则,自己手写很难覆盖全。货币格式还会自动补齐两位小数、加上货币符号。日期、金额等格式为什么不该手拼,见《为什么日期、金额和排序不应该手动拼接格式?》。

手写正则版本

JavaScript
function formatThousands(value) {
  const text = String(value);
  const sign = text.startsWith('-') ? '-' : '';
  const [integer, decimal] = text.replace('-', '').split('.');
  const grouped = integer.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
  return sign + grouped + (decimal === undefined ? '' : `.${decimal}`);
}

console.log(formatThousands(1234567.89)); // 1,234,567.89
console.log(formatThousands(-1234)); // -1,234
console.log(formatThousands(123)); // 123
console.log(formatThousands(0.12345)); // 0.12345
console.log(formatThousands('12345678901234567890')); // 12,345,678,901,234,567,890

正则 \B(?=(\d{3})+(?!\d)) 怎么理解?它匹配的不是字符,而是字符之间的位置。(?=...) 是先行断言,表示“后面要满足某个条件”但不吃掉字符。条件是 (\d{3})+(?!\d):后面紧跟一组或多组三位数字,并且再往后不能还有数字,也就是正好到整数结尾。\B 要求这个位置不在数字串的开头,否则 123456 会在最前面多出一个逗号,变成 ,123,456。

为什么要先拆出小数部分?如果对 1234.5678 整体执行替换,小数点后的 5678 也是数字串,它前面会被插入逗号,变成 1,234.5,678。所以只对整数部分处理。

最后一个例子传的是字符串。如果写成数字 12345678901234567890,它早就超出了安全整数范围,变成 String 后是 12345678901234567000,精度在进入函数前已经丢了。原因见《怎样手写大数相加?为什么 JS 的大整数会算错?》。

常见追问

不用正则怎么写?

从整数部分的末尾开始,每次 slice 出三位放进数组的开头,直到剩下不足三位,再把剩下的部分也放进去,最后 join(',')。思路和列竖式一样,从低位往高位处理。

toFixed 和 Intl.NumberFormat 的小数舍入一样吗?

不完全一样。toFixed 基于数字的二进制近似值舍入,(1.005).toFixed(2) 得到 '1.00',因为 1.005 实际存的值略小于 1.005。金额计算最好先换成以分为单位的整数,展示时再格式化。