Intl.NumberFormat API

使用 JavaScript 内置的国际化 API 按照任意本地化格式化数字

简介

假设你需要在应用中显示数字 1234567.89。如果直接使用 toString(),会得到 "1234567.89",这种格式难以阅读,并且假定所有人都用句点作为小数点且从左到右读数字。美国用户期望 "1,234,567.89",德国用户期望 "1.234.567,89",而印度用户则期望 "12,34,567.89",每个地区的分组规则都不同。

Intl.NumberFormat API 通过根据本地化习惯格式化数字来解决这个问题。它会自动处理千位分隔符、小数点、数字分组、货币符号、百分号、计量单位和数字系统,无需手动字符串处理或第三方库。

本指南将介绍如何使用 Intl.NumberFormat,从基础用法到进阶功能(如货币格式、紧凑表示法和自定义舍入模式),帮助你为全球用户正确格式化数字。

使用默认设置格式化数字

通过调用 new Intl.NumberFormat() 并传入本地化字符串来创建格式化器,然后使用其 format() 方法格式化数字。

const formatter = new Intl.NumberFormat('en-US');
formatter.format(1234567.89);
// "1,234,567.89"

格式化器会根据本地化自动添加千位分隔符并格式化小数点。如果未指定本地化,格式化器会使用运行环境的默认本地化,通常基于用户的系统设置。

const formatter = new Intl.NumberFormat('de-DE');
formatter.format(1234567.89);
// "1.234.567,89"

德国的习惯是用句点作为千位分隔符,用逗号表示小数点,这与美国的习惯正好相反。格式化器会自动处理这些差异。

了解本地化代码

本地化代码用于标识一种语言及可选的地区,格式为 language-REGION。语言部分采用两位 ISO 639-1 代码,如 enes。地区部分采用两位 ISO 3166-1 代码,如 USMX

new Intl.NumberFormat('en-US').format(1234.56);
// "1,234.56" (American English)

new Intl.NumberFormat('en-GB').format(1234.56);
// "1,234.56" (British English)

new Intl.NumberFormat('es-ES').format(1234.56);
// "1234,56" (European Spanish)

new Intl.NumberFormat('es-MX').format(1234.56);
// "1,234.56" (Mexican Spanish)

两种英语变体使用相同的格式,但西班牙语变体有所不同。欧洲西班牙语在四位数时不使用千位分隔符,小数用逗号分隔,而墨西哥西班牙语遵循美式格式。

根据用户的地理位置或语言偏好选择 locale。应用程序通常通过用户设置、浏览器语言或 IP 地理定位来确定 locale。

选择格式化样式

style 选项用于确定格式化类别。请将 options 对象作为构造函数的第二个参数传递。

new Intl.NumberFormat('en-US', {
  style: 'decimal'
}).format(1234.56);
// "1,234.56"

decimal 样式为默认值。其他样式包括 currencypercentunit

使用符号和代码格式化货币

currency 样式需要一个带有 ISO 4217 货币代码的 currency 选项。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).format(1234.56);
// "$1,234.56"

格式化器默认会添加美元符号,并保留两位小数,这是大多数货币的标准。不同的 locale 会有不同的货币符号位置。

new Intl.NumberFormat('de-DE', {
  style: 'currency',
  currency: 'EUR'
}).format(1234.56);
// "1.234,56 €"

德语格式会在金额后加上欧元符号并留有空格。currency 选项决定显示哪种货币,而不是采用哪个 locale 的格式规范。locale 决定格式规范,currency 决定货币符号。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'EUR'
}).format(1234.56);
// "€1,234.56"

美式格式规范下,欧元符号会出现在金额前,遵循美式而非欧洲的符号位置规则。

控制货币显示格式

currencyDisplay 选项可更改货币在格式化字符串中的显示方式。

const amount = 1234.56;

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'symbol'
}).format(amount);
// "$1,234.56"

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'code'
}).format(amount);
// "USD 1,234.56"

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'name'
}).format(amount);
// "1,234.56 US dollars"

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'narrowSymbol'
}).format(amount);
// "$1,234.56"

symbol 选项为默认值,会显示货币符号,如 $code 选项会显示三字母货币代码。name 选项会拼写出货币名称。narrowSymbol 选项会使用 locale 的窄货币符号,虽然可能不唯一,但可以节省空间。

当涉及到具有相同货币符号的货币时,symbolnarrowSymbol 之间的区别就会变得明显。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'CAD',
  currencyDisplay: 'symbol'
}).format(100);
// "CA$100.00"

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'CAD',
  currencyDisplay: 'narrowSymbol'
}).format(100);
// "$100.00"

加拿大元在使用 symbol 选项时会显示为 CA$,以便与美元区分;而使用 narrowSymbol 时则仅显示为 $

使用会计记账法格式化负数货币金额

currencySign 选项用于控制负数金额的显示方式。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencySign: 'standard'
}).format(-1234.56);
// "-$1,234.56"

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD',
  currencySign: 'accounting'
}).format(-1234.56);
// "($1,234.56)"

standard 选项为默认值,使用负号表示负数金额。accounting 选项则将负数金额用括号括起来,符合会计惯例。这使得财务报表中的负数更加醒目。

百分比格式化

percent 样式会将数字乘以 100 并添加百分号。

new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(0.1234);
// "12%"

new Intl.NumberFormat('en-US', {
  style: 'percent'
}).format(0.1256);
// "13%"

格式化器默认会四舍五入到最接近的整数。可以通过数字选项控制小数位数。

new Intl.NumberFormat('en-US', {
  style: 'percent',
  minimumFractionDigits: 2
}).format(0.1234);
// "12.34%"

请传入百分比的小数形式,而不是已乘以 100 的值。格式化器会自动处理乘法。

带单位的数值格式化

unit 样式需要一个带有单位标识符的 unit 选项。

new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer-per-hour'
}).format(100);
// "100 km/h"

new Intl.NumberFormat('en-GB', {
  style: 'unit',
  unit: 'mile-per-hour'
}).format(100);
// "100 mph"

可用单位包括长度(meterkilometermile)、时间(secondminutehour)、数字存储(bytekilobytemegabyte)、温度(celsiusfahrenheit)等。复合单位如 kilometer-per-hour 则通过连字符组合简单单位。

unitDisplay 选项用于控制单位的格式。

const distance = 1234.5;

new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'long'
}).format(distance);
// "1,234.5 kilometers"

new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'short'
}).format(distance);
// "1,234.5 km"

new Intl.NumberFormat('en-US', {
  style: 'unit',
  unit: 'kilometer',
  unitDisplay: 'narrow'
}).format(distance);
// "1,234.5km"

long 选项会完整拼写单位名称。short 选项使用缩写。narrow 选项采用最简形式,可能存在歧义。

使用紧凑记数法显示大数字

notation 选项会改变数字的表达方式。compact 值会针对大数字使用特定语言环境的简写形式。

new Intl.NumberFormat('en-US', {
  notation: 'compact'
}).format(1234567);
// "1.2M"

new Intl.NumberFormat('en-US', {
  notation: 'compact'
}).format(987654321);
// "988M"

紧凑记数法默认保留一位小数,并为千位 K、百万 M 和十亿 B 添加后缀。这种格式常见于社交媒体粉丝数、视频播放量和分析仪表盘。

compactDisplay 选项用于控制后缀长度。

new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'short'
}).format(1234567);
// "1.2M"

new Intl.NumberFormat('en-US', {
  notation: 'compact',
  compactDisplay: 'long'
}).format(1234567);
// "1.2 million"

short 选项为默认值,使用符号。long 选项会拼写数量级单词。不同语言环境会采用不同的后缀。

new Intl.NumberFormat('zh-CN', {
  notation: 'compact'
}).format(123456789);
// "1.2亿"

中文使用 亿 表示亿,反映了中文的数字分组体系。

用科学计数法表示极大或极小的数字

scientific 记数法将数字表示为系数与 10 的幂的乘积。

new Intl.NumberFormat('en-US', {
  notation: 'scientific'
}).format(123456789);
// "1.235E8"

new Intl.NumberFormat('en-US', {
  notation: 'scientific'
}).format(0.00000123);
// "1.23E-6"

这种格式适用于极大的数字(如天文距离、分子数量)和极小的数字(如粒子质量、纳米级测量)。指数始终以 1 的倍数出现。

在技术应用中使用工程计数法

engineering 记数法类似于科学计数法,但指数限定为 3 的倍数。

new Intl.NumberFormat('en-US', {
  notation: 'engineering'
}).format(123456789);
// "123.457E6"

new Intl.NumberFormat('en-US', {
  notation: 'engineering'
}).format(1234);
// "1.234E3"

工程计数法与 SI 单位前缀(kilo、mega、giga)对齐,因此在工程和物理领域是标准做法。系数范围为 1 到 999。

使用小数位数控制小数精度

minimumFractionDigitsmaximumFractionDigits 选项用于控制小数点后显示的位数。

new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}).format(1234.5);
// "1,234.50"

new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 2,
  maximumFractionDigits: 2
}).format(1234.567);
// "1,234.57"

最小值确保在需要时补零,最大值则会对较长的小数进行四舍五入。货币格式化默认保留两位小数,数字格式化默认最小为零位,最大为三位。

new Intl.NumberFormat('en-US', {
  minimumFractionDigits: 0,
  maximumFractionDigits: 0
}).format(1234.567);
// "1,235"

将两者都设置为零时,会四舍五入到最接近的整数。

使用有效数字控制整体精度

minimumSignificantDigitsmaximumSignificantDigits 选项用于控制总精度,与小数点位置无关。

new Intl.NumberFormat('en-US', {
  minimumSignificantDigits: 3,
  maximumSignificantDigits: 3
}).format(1234.567);
// "1,230"

new Intl.NumberFormat('en-US', {
  minimumSignificantDigits: 3,
  maximumSignificantDigits: 3
}).format(0.001234);
// "0.00123"

有效数字统计除前导零外的所有数字。第一个示例会四舍五入到三位有效数字,结果为 1230。第二个示例会保留前导零后的三位数字,结果为 0.00123

当同时指定有效数字和小数位数选项时,有效数字选项优先生效。

通过舍入模式选择舍入策略

roundingMode 选项决定在需要截断时数字的舍入方式。

const value = 1.5;

new Intl.NumberFormat('en-US', {
  maximumFractionDigits: 0,
  roundingMode: 'halfExpand'
}).format(value);
// "2"

new Intl.NumberFormat('en-US', {
  maximumFractionDigits: 0,
  roundingMode: 'halfTrunc'
}).format(value);
// "1"

halfExpand 模式为默认值,会将 0.5 向零的远离方向舍入,这是学校常用的舍入策略。halfTrunc 模式则将 0.5 向零的方向舍入。

其他模式包括:

  • ceil:始终向正无穷舍入
  • floor:始终向负无穷舍入
  • expand:始终远离零舍入
  • trunc:始终向零舍入
  • halfCeil:0.5 向正无穷舍入
  • halfFloor:0.5 向负无穷舍入
  • halfEven:0.5 向最接近的偶数舍入
const prices = [1.5, 2.5, 3.5];

prices.map(price =>
  new Intl.NumberFormat('en-US', {
    maximumFractionDigits: 0,
    roundingMode: 'halfEven'
  }).format(price)
);
// ["2", "2", "4"]

halfEven 模式,也称为银行家舍入法,可减少重复计算中的舍入偏差。当对 0.5 进行舍入时,会选择最接近的偶数。这会使 2 同时适用于 1.52.5,但 4 适用于 3.5

金融应用中,ceil 用于向上舍入费用,floor 用于向下舍入退款。统计应用中,halfEven 用于最小化累计舍入误差。

通过分组选项控制千位分隔符

useGrouping 选项用于控制是否显示千位分隔符。

new Intl.NumberFormat('en-US', {
  useGrouping: true
}).format(123456);
// "123,456"

new Intl.NumberFormat('en-US', {
  useGrouping: false
}).format(123456);
// "123456"

true 是默认值。false 值会移除所有分隔符。字符串值可实现更精细的控制。

new Intl.NumberFormat('en-US', {
  useGrouping: 'always'
}).format(1234);
// "1,234"

new Intl.NumberFormat('en-US', {
  useGrouping: 'min2'
}).format(1234);
// "1234"

always 值在所有情况下都使用分隔符。min2 值会在四位数字时省略分隔符。auto 值遵循本地化偏好,通常与 min2 行为一致。

紧凑表示法默认采用 min2,因为紧凑数字很少需要内部分隔符。

通过符号显示选项显式显示正负号

signDisplay 选项用于控制何时显示正负号。

new Intl.NumberFormat('en-US', {
  signDisplay: 'auto'
}).format(100);
// "100"

new Intl.NumberFormat('en-US', {
  signDisplay: 'always'
}).format(100);
// "+100"

auto 是默认值,负数显示负号,正数不显示正号。always 值则两者都显示。

new Intl.NumberFormat('en-US', {
  signDisplay: 'exceptZero'
}).format(0);
// "0"

new Intl.NumberFormat('en-US', {
  signDisplay: 'always'
}).format(0);
// "+0"

exceptZero 值即使在 always 行为下也会对零值省略符号。这可以避免 +0-0 的混淆显示。

new Intl.NumberFormat('en-US', {
  signDisplay: 'never'
}).format(-100);
// "100"

never 值会完全移除符号,仅显示绝对值。

金融应用使用 always 通过加号突出显示收益。温度显示使用 exceptZero 避免显示 +0°-0°

将格式化输出拆分为多个部分

formatToParts() 方法返回一个对象数组,每个对象代表格式化字符串的一个组成部分。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatToParts(1234.56);

返回结果为:

[
  { type: 'currency', value: '$' },
  { type: 'integer', value: '1' },
  { type: 'group', value: ',' },
  { type: 'integer', value: '234' },
  { type: 'decimal', value: '.' },
  { type: 'fraction', value: '56' }
]

每个对象包含一个 type 用于标识组件,以及一个 value 存储字符串。type 可以是 currencyintegergroupdecimalfractionliteralminusSignplusSignpercentSign 或其他。

这样可以对各个组件进行自定义样式。

const parts = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatToParts(1234.56);

const formatted = parts.map(part => {
  if (part.type === 'currency') {
    return `<span class="currency">${part.value}</span>`;
  }
  if (part.type === 'integer') {
    return `<span class="integer">${part.value}</span>`;
  }
  return part.value;
}).join('');

// <span class="currency">$</span><span class="integer">1</span>,<span class="integer">234</span>.56

这会生成带有样式组件的 HTML。相同方法也适用于为负数金额设置不同颜色、放大货币符号或为单个数字添加动画。

格式化数字区间

formatRange() 方法可将两个数字格式化为区间。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatRange(100, 200);
// "$100.00 – $200.00"

格式化器会使用特定于本地的区间分隔符(英文中为短横线),并包含两个货币符号。如果两个值格式化后字符串相同,则格式化器返回带有波浪号的单个值。

new Intl.NumberFormat('en-US', {
  notation: 'compact'
}).formatRange(1200, 1800);
// "~1K"

12001800 在紧凑表示法下都格式化为 1K,因此格式化器会显示大约 1K

formatRangeToParts() 方法返回区间的各个部分。

new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
}).formatRangeToParts(100, 200);

返回结果为:

[
  { type: 'currency', value: '$', source: 'startRange' },
  { type: 'integer', value: '100', source: 'startRange' },
  { type: 'decimal', value: '.', source: 'startRange' },
  { type: 'fraction', value: '00', source: 'startRange' },
  { type: 'literal', value: ' – ', source: 'shared' },
  { type: 'currency', value: '$', source: 'endRange' },
  { type: 'integer', value: '200', source: 'endRange' },
  { type: 'decimal', value: '.', source: 'endRange' },
  { type: 'fraction', value: '00', source: 'endRange' }
]

source 属性用于标识该部分是来自起始值、结束值还是区间分隔符。

价格区间、日期区间和测量区间都会使用此方法,以实现符合本地化的格式化。

检查已解析的选项

resolvedOptions() 方法会返回一个对象,显示格式化器实际使用的选项。

const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

formatter.resolvedOptions();

返回结果为:

{
  locale: 'en-US',
  numberingSystem: 'latn',
  style: 'currency',
  currency: 'USD',
  currencyDisplay: 'symbol',
  currencySign: 'standard',
  minimumIntegerDigits: 1,
  minimumFractionDigits: 2,
  maximumFractionDigits: 2,
  useGrouping: 'auto',
  notation: 'standard',
  signDisplay: 'auto',
  roundingMode: 'halfExpand'
}

该对象包含显式设置的选项和默认值。如果系统解析为不同但兼容的区域设置,locale 可能与请求的区域设置不同。numberingSystem 显示格式化器使用的数字字符。

此方法通过展示所有激活的设置,有助于调试意外的格式化结果。

复用格式化器以提升性能

创建 NumberFormat 实例时会加载本地化数据并处理选项。当需要用相同设置格式化多个值时,建议复用实例。

// Inefficient: creates new formatter for each value
prices.map(price =>
  new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(price)
);

// Efficient: creates formatter once
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

prices.map(price => formatter.format(price));

在格式化大量数据时,第二种方式明显更快。请在循环和组件渲染函数外部创建格式化器。

现代 JavaScript 引擎会在内部缓存 NumberFormat 实例,但显式复用能带来更好的性能和更清晰的代码。

检查浏览器支持情况

Intl.NumberFormat API 已在所有现代浏览器中获得支持。Chrome、Firefox、Safari 和 Edge 自 2016 年起支持基础 API。像 formatRange()formatRangeToParts() 以及扩展的 roundingMode 选项等高级功能,近年也已在当前浏览器版本中支持。

可通过以下方式检查支持情况:

if (typeof Intl !== 'undefined' && Intl.NumberFormat) {
  // NumberFormat is supported
  const formatter = new Intl.NumberFormat('en-US');
}

检查特定功能支持:

const formatter = new Intl.NumberFormat('en-US');

if (typeof formatter.formatRange === 'function') {
  // formatRange is supported
}

需要支持旧版浏览器的应用可以使用如 @formatjs/intl-numberformat 这样的 polyfill,但大多数现代应用可以直接使用原生 API,无需回退方案。

何时使用 NumberFormat

在以下场景使用 Intl.NumberFormat

  • 在任何 UI 场景下向用户展示数字
  • 在电商应用中格式化货币金额
  • 在分析仪表盘中显示百分比
  • 展示粉丝数、浏览量等社交指标
  • 格式化度量值和科学数值
  • 构建支持多语言环境的国际化应用

不要在以下场景使用 Intl.NumberFormat

  • 内部计算或数据处理
  • 在数据库中存储数值
  • 为 API 序列化数据
  • 将用户输入解析回数字

NumberFormat 是一个展示层工具。请在计算和存储时保留原始数值,仅在向用户展示时进行格式化。