如何获取格式化数字的各个部分以实现自定义显示

将格式化数字拆分为组件,便于应用自定义样式并构建复杂界面

简介

format() 方法会返回一个完整的格式化字符串,例如 "$1,234.56" 或 "1.5M"。这种方式适用于简单的展示,但无法对各个部分分别设置样式。你无法将货币符号加粗、将小数部分着色,或为特定组件应用自定义标记。

JavaScript 提供了 formatToParts() 方法来解决这个问题。它不会返回单一字符串,而是返回一个对象数组,每个对象代表格式化数字的一个部分。每个部分都有一个类型,比如 currencyintegerdecimal,以及一个值,比如 $1234.。你可以处理这些部分,实现自定义样式、构建复杂布局,或将格式化数字集成到丰富的用户界面中。

为什么格式化字符串难以自定义

当你收到一个像 "$1,234.56" 这样的格式化字符串时,很难准确区分货币符号和数字的起止位置。不同的本地化环境会将符号放在不同的位置,有些地区还会使用不同的分隔符。要可靠地解析这些字符串,需要非常复杂的逻辑,这实际上是在重复 Intl API 已经实现的格式化规则。

假设你有一个仪表盘,需要将货币符号显示为不同的颜色。如果使用 format(),你需要:

  1. 检测哪些字符是货币符号
  2. 处理符号与数字之间的空格
  3. 兼容不同本地化环境下的符号位置
  4. 仔细解析字符串,避免破坏数字本身

这种方法不够健壮且容易出错。只要本地化格式规则发生变化,你的解析逻辑就会被破坏。

formatToParts() 方法通过分别提供各个组件,彻底解决了这个问题。你会收到结构化数据,能够准确知道每个部分的含义,无论使用哪种本地化格式。

使用 formatToParts 获取数字组件

formatToParts() 方法的工作方式与 format() 完全相同,唯一的区别在于返回值。你可以用相同的选项创建格式化器,然后调用 formatToParts(),而不是 format()

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);

这会输出一个对象数组:

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

每个对象都包含一个 type 属性,用于标识该部分的类型,以及一个 value 属性,包含实际的字符串。各部分的顺序与格式化输出时一致。

你可以通过将所有值拼接起来进行验证:

const formatted = parts.map(part => part.value).join("");
console.log(formatted);
// Output: "$1,234.56"

拼接后的各部分会生成与调用 format() 完全相同的输出。

了解各部分类型

type 属性用于标识每个组件。不同的格式化选项会产生不同的部分类型。

对于基础数字格式化:

const formatter = new Intl.NumberFormat("en-US");
const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

integer 类型表示整数部分。当数字被分组分隔符分割时,会出现多个 integer 部分。group 类型表示千位分隔符。decimal 类型表示小数点。fraction 类型表示小数点后的数字。

对于货币格式化:

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

const parts = formatter.formatToParts(1234.56);
console.log(parts);
// [
//   { type: "currency", value: "€" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

currency 类型会根据本地化习惯出现在数字前或后。

关于百分比:

const formatter = new Intl.NumberFormat("en-US", {
  style: "percent"
});

const parts = formatter.formatToParts(0.1234);
console.log(parts);
// [
//   { type: "integer", value: "12" },
//   { type: "percentSign", value: "%" }
// ]

percentSign 类型表示百分号。

关于紧凑记数法:

const formatter = new Intl.NumberFormat("en-US", {
  notation: "compact"
});

const parts = formatter.formatToParts(1500000);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "5" },
//   { type: "compact", value: "M" }
// ]

compact 类型表示数量级指示符,如 K、M 或 B。

为数字部分应用自定义样式

formatToParts() 的主要用例是为不同组件应用不同样式。你可以处理 parts 数组,将特定类型包裹在 HTML 元素中。

将货币符号加粗:

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

const parts = formatter.formatToParts(1234.56);
const html = parts
  .map(part => {
    if (part.type === "currency") {
      return `<strong>${part.value}</strong>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "<strong>$</strong>1,234.56"

这种方法适用于任何标记语言。你可以通过处理 parts 数组生成 HTML、JSX 或其他格式。

为小数部分设置不同样式:

const formatter = new Intl.NumberFormat("en-US", {
  minimumFractionDigits: 2
});

const parts = formatter.formatToParts(1234.5);
const html = parts
  .map(part => {
    if (part.type === "decimal" || part.type === "fraction") {
      return `<span class="text-gray-500">${part.value}</span>`;
    }
    return part.value;
  })
  .join("");

console.log(html);
// Output: "1,234<span class="text-gray-500">.50</span>"

这种模式在价格展示中很常见,小数部分通常显示为较小或较浅的样式。

负数的颜色标记

金融应用通常将负数显示为红色。使用 formatToParts(),你可以检测负号并相应地应用样式。

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

function formatWithColor(number) {
  const parts = formatter.formatToParts(number);
  const hasMinusSign = parts.some(part => part.type === "minusSign");

  const html = parts
    .map(part => part.value)
    .join("");

  if (hasMinusSign) {
    return `<span class="text-red-600">${html}</span>`;
  }

  return html;
}

console.log(formatWithColor(-1234.56));
// Output: "<span class="text-red-600">-$1,234.56</span>"

console.log(formatWithColor(1234.56));
// Output: "$1,234.56"

这种方法可以在所有本地化环境下可靠地检测负数,即使不同地区使用不同的符号或负号位置。

使用多种样式构建自定义数字显示

复杂界面通常会结合多种样式规则。你可以同时为不同部分类型应用不同的类或元素。

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

function formatCurrency(number) {
  const parts = formatter.formatToParts(number);

  return parts
    .map(part => {
      switch (part.type) {
        case "currency":
          return `<span class="currency-symbol">${part.value}</span>`;
        case "integer":
          return `<span class="integer">${part.value}</span>`;
        case "group":
          return `<span class="group">${part.value}</span>`;
        case "decimal":
          return `<span class="decimal">${part.value}</span>`;
        case "fraction":
          return `<span class="fraction">${part.value}</span>`;
        case "minusSign":
          return `<span class="minus">${part.value}</span>`;
        default:
          return part.value;
      }
    })
    .join("");
}

console.log(formatCurrency(1234.56));
// Output: "<span class="currency-symbol">$</span><span class="integer">1</span><span class="group">,</span><span class="integer">234</span><span class="decimal">.</span><span class="fraction">56</span>"

这种细粒度的控制可以实现每个组件的精确样式。你可以使用 CSS 分别设置每个类的样式。

所有可用的部分类型

type 属性根据所用格式化选项,可能具有以下值:

  • integer:整数位数字
  • fraction:小数位数字
  • decimal:小数点分隔符
  • group:千位分隔符
  • currency:货币符号
  • literal:格式化时添加的空格或其他文本
  • percentSign:百分号
  • minusSign:负数指示符
  • plusSign:正数指示符(当 signDisplay 被设置时)
  • unit:单位格式化时的单位字符串
  • compact:紧凑记数法中的数量级指示符(K、M、B)
  • exponentInteger:科学计数法中的指数值
  • exponentMinusSign:指数中的负号
  • exponentSeparator:尾数与指数之间的分隔符
  • infinity:无穷大表示
  • nan:非数字(NaN)表示
  • unknown:未识别的标记

并非每种格式化选项都会生成所有类型的部分。实际返回的部分取决于数值和格式化器的配置。

科学计数法会生成与指数相关的部分:

const formatter = new Intl.NumberFormat("en-US", {
  notation: "scientific"
});

const parts = formatter.formatToParts(1234);
console.log(parts);
// [
//   { type: "integer", value: "1" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "234" },
//   { type: "exponentSeparator", value: "E" },
//   { type: "exponentInteger", value: "3" }
// ]

特殊值会生成特定类型的部分:

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

console.log(formatter.formatToParts(Infinity));
// [{ type: "infinity", value: "∞" }]

console.log(formatter.formatToParts(NaN));
// [{ type: "nan", value: "NaN" }]

创建无障碍数字显示

你可以使用 formatToParts() 为格式化后的数字添加无障碍属性。这有助于屏幕阅读器正确朗读数值。

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

function formatAccessibleCurrency(number) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  return `<span aria-label="${number} US dollars">${formatted}</span>`;
}

console.log(formatAccessibleCurrency(1234.56));
// Output: "<span aria-label="1234.56 US dollars">$1,234.56</span>"

这样可以确保屏幕阅读器在朗读时,既能传达格式化后的显示值,也能传达底层的数值及其上下文。

高亮特定数字区间

某些应用会高亮显示落在特定区间的数字。通过 formatToParts(),你可以在保持正确格式的同时,根据数值应用样式。

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

function formatWithThreshold(number, threshold) {
  const parts = formatter.formatToParts(number);
  const formatted = parts.map(part => part.value).join("");

  if (number >= threshold) {
    return `<span class="text-green-600 font-bold">${formatted}</span>`;
  }

  return formatted;
}

console.log(formatWithThreshold(1500, 1000));
// Output: "<span class="text-green-600 font-bold">1,500</span>"

console.log(formatWithThreshold(500, 1000));
// Output: "500"

数字会根据本地化规则正确格式化,同时根据业务逻辑应用条件样式。

何时使用 formatToParts 与 format

当你只需要一个简单的格式化字符串且无需自定义时,建议使用 format()。这也是大多数数字展示的常见场景。

当你需要:

  • 对数字的不同部分应用不同样式
  • 用格式化数字构建 HTML 或 JSX
  • 为特定组件添加属性或元数据
  • 将格式化数字集成到复杂布局中
  • 以编程方式处理格式化输出

formatToParts() 方法比 format() 稍微有更多开销,因为它会生成对象数组而不是单一字符串。对于典型应用来说,这种差异可以忽略,但如果你每秒需要格式化成千上万个数字,format() 的性能更优。

对于大多数应用程序,应根据样式需求而非性能考虑进行选择。如果不需要自定义输出,请使用 format() 。如果需要自定义样式或标记,请使用 formatToParts()

各部分如何保留特定语言环境的格式

parts 数组会自动维护特定语言环境的格式规则。不同的语言环境会将符号放在不同的位置,并使用不同的分隔符,但 formatToParts() 能够处理这些差异。

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

console.log(usdFormatter.formatToParts(1234.56));
// [
//   { type: "currency", value: "$" },
//   { type: "integer", value: "1" },
//   { type: "group", value: "," },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "." },
//   { type: "fraction", value: "56" }
// ]

const eurFormatter = new Intl.NumberFormat("de-DE", {
  style: "currency",
  currency: "EUR"
});

console.log(eurFormatter.formatToParts(1234.56));
// [
//   { type: "integer", value: "1" },
//   { type: "group", value: "." },
//   { type: "integer", value: "234" },
//   { type: "decimal", value: "," },
//   { type: "fraction", value: "56" },
//   { type: "literal", value: " " },
//   { type: "currency", value: "€" }
// ]

德语格式会在数字后加上货币符号,并用空格分隔。千位分隔符为句点,小数分隔符为逗号。无论语言环境如何,您的样式代码都以相同方式处理 parts 数组,格式会自动适配。

literal 类型表示格式化器插入的、不属于其他类别的任何空格或文本。在德语货币格式中,它表示数字和货币符号之间的空格。

将 formatToParts 与框架组件结合使用

现代框架(如 React )可以使用 formatToParts() 高效构建组件。

function CurrencyDisplay({ value, locale, currency }) {
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: currency
  });

  const parts = formatter.formatToParts(value);

  return (
    <span className="currency-display">
      {parts.map((part, index) => {
        if (part.type === "currency") {
          return <strong key={index}>{part.value}</strong>;
        }
        if (part.type === "fraction" || part.type === "decimal") {
          return <span key={index} className="text-sm text-gray-500">{part.value}</span>;
        }
        return <span key={index}>{part.value}</span>;
      })}
    </span>
  );
}

该组件可以为不同部分应用不同样式,同时保持任何语言环境和货币的正确格式。