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"(美式英语)

new Intl.NumberFormat('en-GB').format(1234.56);
// "1,234.56"(英式英语)

new Intl.NumberFormat('es-ES').format(1234.56);
// "1234,56"(欧洲西班牙语)

new Intl.NumberFormat('es-MX').format(1234.56);
// "1,234.56"(墨西哥西班牙语)

两种英语变体使用相同的格式,但西班牙语变体有所不同。欧洲西班牙语对四位数省略千位分隔符,并使用逗号作为小数点,而墨西哥西班牙语遵循美国的约定。

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

选择格式化样式

style 选项决定了格式化的类别。将一个选项对象作为第二个参数传递给构造函数。

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"

格式化器会添加美元符号,并默认格式化为两位小数,这是大多数货币的标准。不同的区域会以不同的方式放置符号。

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

德语格式会在金额后加上欧元符号,并留有一个空格。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 选项使用区域的窄货币符号,可能会有歧义,但节省空间。

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 公里"

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 表示法将数字表示为系数乘以十的幂。

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)前缀(如千、兆、吉)对齐,因此在工程和物理学领域中是标准格式。系数范围为 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"

minimumFractionDigits 确保在需要时显示尾随零。maximumFractionDigits 则会对较长的小数进行四舍五入。货币格式化器默认使用两位小数。小数格式化器默认的最小位数为零,最大位数为三。

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 时,它选择最接近的偶数。这使得 1.52.5 的结果为 2,而 3.5 的结果为 4

金融应用中使用 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 实例需要加载区域设置数据和处理选项。当使用相同设置格式化多个值时,请重用实例。

// 效率低:每个值都创建新的格式化器
prices.map(price =>
  new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD'
  }).format(price)
);

// 高效:只创建一次格式化器
const formatter = new Intl.NumberFormat('en-US', {
  style: 'currency',
  currency: 'USD'
});

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

第二种方法在格式化多个值时显著更快。在循环和组件渲染函数外部创建格式化器。

现代 JavaScript 引擎会在内部缓存 NumberFormat 实例,但显式重用可以提供更好的性能和更清晰的代码。

检查浏览器支持

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

可以使用以下方法检查支持情况:

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

检查特定功能:

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

if (typeof formatter.formatRange === 'function') {
  // 支持 formatRange
}

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

何时使用 NumberFormat

使用 Intl.NumberFormat 的场景:

  • 在任何用户界面上下文中向用户显示数字
  • 在电子商务应用中格式化货币金额
  • 在分析仪表板中显示百分比
  • 显示关注者数量、观看次数和其他社交指标
  • 格式化测量值和科学数值
  • 构建支持多种语言环境的国际化应用程序

不要将 Intl.NumberFormat 用于:

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

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