【TA不知道的JS】toLocaleString

1,453 阅读12分钟

我正在参加「掘金·启航计划」

我有一个朋友叫小帅,是位前端开发工程师。

这天,小帅在开发一个数据展示的功能时遇到了点小问题。

产品希望将表格中金额数字展示为逗号分隔的形式,例如123456展示为123,456

小帅简单🤔了一下,手动去造一个数字处理的轮子属实没必要,但是为了这么个小需求去引入第三方库好像也有点说不过去,于是小帅开始面露难色,有没有比较简单的又不用引入第三方库的方法呢?

此时,旁边的小美悠悠地来了一句:难道你不知道JS中有个方法是toLocaleString吗❓


Number.prototype.toLocaleString()

定义

toLocaleString() 方法返回这个数字在特定语言环境下的表示字符串。

MDN 上的定义永远都是那么简单,然而越简单就越难完全理解其中的含义。将数字转换为字符串这个很容易理解,那么特定语言环境下是什么环境呢,它跟 toString() 方法有什么区别呢❓

实践是检验真理的唯一标准。

让我们打开 Chrome 的开发者工具,修改浏览器的 locale,操作流程如下所示:

  1. 通过 F12 或者 Command + Option + i 打开浏览器的开发者工具;
  2. 按下 ESC 唤起开发者工具控制台的隐藏抽屉;
  3. 在隐藏抽屉 Tab 栏选择传感器(英文叫 sensor)进入位置设置面板;
  4. 下拉选择并替换位置(英文叫 Location)中选项;
  5. 在控制台中调用待测试的 toLocaleString() 代码并查看输出结果。

Sep-20-2022 18-04-23.gif

在不同的 locale 环境下分别对 toLocaleString()toString() 方法进行测试,得到以下结果:

// 默认环境下,locale为zh-CN
(123456).toLocaleString() === '123,456'
(123456).toString() === '123456'

// 设置location为 London,locale值为en-GB
(123456).toLocaleString() === '123,456'
(123456).toString() === '123456'

// 设置location为 Tokyo,locale值为ja-JP
(123456).toLocaleString() === '123,456'
(123456).toString() === '123456'

// 设置location为 Sao Paulo,locale值为pt-BR
(123456).toLocaleString() === '123.456'
(123456).toString() === '123456'

从测试的结果我们可以知道:toLocaleString() 方法会根据不同的语言环境将数字转为相适应的更加易读的字符串,而 toString() 方法是按照JS标准进行转换,不受语言环境影响。

用法

toLocaleString() 的初代方法中,不支持任意参数,返回结果依赖用户的语言环境及浏览器对于该方法的实现方式。到了 ECMA402 中新增了 localesoptions 这两个参数的支持,允许用户进行指定要进行格式转换的语言并定制转换函数的行为。

目前主流浏览器都是支持 localesoptions 这两个参数的,所以在项目中可以放心使用。

使用方法如下所示:

MDN 中对于参数 localesoptions 的描述相当复杂,这里不再赘述,只对其中模棱两可的地方以及关键知识点做简单的总结。

locales

MDN 上的定义可以简单总结为一句话:缩写语言代码(BCP 47 language tag,例如: cmn-Hans-CN)的字符串或者这些字符串组成的数组。 所以这里最关键的就是要搞清楚什么是 BCP 47 language tag

BCP 47(后面都省略掉语言标记) 用于表示一种语言或者区域,它由以下几部分组成:

[语言代码]-[?脚本代码]-[?国家代码]-[?Unicode扩展-[格式化类型]-[格式化方式]]
组成部分解释可省略参数示例
语言代码IANA 语言子标记注册中的 Type 值为 language 的标签值zh(中文)
en(英文)
脚本代码IANA 语言子标记注册中的 Type 值为 script 的标签值zh-Hans(使用简体中文)
国家代码IANA 语言子标记注册中的 Type 值为 region 的标签值zh-Hans-CN(使用中国地区的简体中文)
Unicode扩展通过 u- 开头指定使用不同的格式化类型进行数据格式化zh-Hans-CN-u-nu-hanidec(使用中国地区简体中文的十进制方式格式化数据)
格式化类型CollatorNumberFormat,或者 DateTimeFormat 对象对应的标签值(conuca必须在 u- 之后使用th-TH-u-nu-thai(使用泰语的数值方式格式化数据)
自定义区域虽然也表示区域,但与国家代码不一样,常以区域全名展示,且只与 Unicode扩展 有关必须在 u-[格式化类型]- 之后使用ja-JP-u-ca-japanese(使用日本的日期方式格式化数据)

关于 BCP 47 更加详细的介绍可以查看官方的RFC文件,这里不再继续深入。下面给出不同参数的示例代码:

细心的同学可以发现在示例代码中最后一项,我们使用了 new Date().toLocaleString() 方法,这是因为所有的 toLocaleString() 方法都使用相同的 locales 定义,也不是所有的 locales 参数都能在 Number.prototype.toLocaleString() 方法中生效。

最后补充知识点:当我们将多个 locales 作为数组传入到 toLocaleString() 方法中时,处理过程中会依次按序使用 locales 配置,若前一个配置失效,便会使用下一个

options

第二个参数 options 支持的参数较多,并且随着 ECMASript 的更新还会不断地往里添加,目前主流浏览器支持的参数有以下这些:

参数名称参数类型默认值说明参数示例
localeMatcher"lookup" | "best fit""best fit"使用的 local 匹配算法,详细可以查看 Intl.LocaleMatcher{ localeMatcher: "lookup" }
style"decimal" | "currency" | "percent" | "unit""decimal"使用的格式样式:
decimal - 纯数字格式
currency - 货币格式
percent - 百分比格式
unit - 单位格式
🛑 使用 currency 和 unit 时必须指定对应的 currency 和 unit 属性
{ style: "percent" }
currencyISO 中的货币代码undefined在货币格式化中使用的货币符号,例如 CNY 会被转换成 { style: "currency", currency: "CNY" }
currencyDisplay"symbol" | "code" | "name""symbol"以何种方式显示货币:
symbol - 本地化的货币符号,如
code - 国际标准货币代码,如CNY
name - 本地化的货币名称,如人民币
{ style: "currency", currency: "CNY", currencyDisplay: "name" }
currencySign"accounting" | "standard""standard"负数的处理,有些 locales 下负数不是使用-表示,此时需要设置值为 accounting{ style: "currency", currency: "CNY", currencySign: "accounting" }
useGroupingtrue | falsetrue是否使用分组分隔符,如千位分隔符,或其他千/万/亿分隔符{ useGrouping: true }
numberingSystem编号系统可选项undefined格式化使用的编号系统,与 locales 中的格式化类型相同{ numberingSystem: "hans" }
unit单位标识符undefined作为格式化后展示的单位,通常在 style 属性为 unit 时使用{ style: "unit", unit: "day" }
unitDisplay"long" | "short" | "narrow""short"必须在有 unit 属性时使用,用于格式化单位的样式{ style: "unit", unit: "day", unitDisplay: "long" }
notation"standard" | "scientific" | "engineering" | "compact""standard"使用的计数法方式,科学计数法及其他本地化的计数方式等{ notation: "compact" }
compactDisplay"short" | "long""short"notation 属性值为 compact 时使用,用于格式化计数法展示方式{ notation: "compact", compactDisplay: "long" }
signDisplay"auto" | "always" | "exceptZero" | "negative" | "nerver""auto"控制数字符号的展示:
auto - 只有负数的时候展示符号,包括负零
always - 总是显示符号
exceptZero - 除了零都展示符号
negative - 只有负数的时候展示符号,不包括负零
never - 始终不展示符号
{ signDisplay: "always" }
minimumIntegerDigitsnumber1默认使用的整数数字的最小位数,不够的话会自动补 0,范围为 1 - 21{ minimumIntegerDigits: 21 }
minimumFractionDigitsnumber0默认使用的小数数字的最小位数,不够的话自动补 0,范围为 0 - 20{ minimumIntegerDigits: 20 }
maximumFractionDigitsnumber3默认使用的小数数字的最大位数,取值范围为 0 - 20,纯数字格式的默认值是和3中大的那一个;货币格式默认值是和 ISO 4217 currency code list 中大的那一个 (如果列表中没有提供则值为 2);和0中大的那一个{ maximumFractionDigits: 20 }
minimumSignificantDigitsnumber1使用的有效数字的最小位数,取值范围为 1 - 21{ minimumSignificantDigits: 21 }
maximumSignificantDigitsnumber21使用的有效数字的最大位数,取值范围为 1 - 21{ maximumSignificantDigits: 1 }

常见用法

toLocaleString() 方法提供了丰富的参数以满足不同场景下的各类数字转换,使我们不再需要类似 numeral.js 等格式转换库。这里给出部分常见的使用 toLocaleString() 进行数字转换的方法。

金额展示

const priceNum = 99998;

// 以普通的逗号形式展示,例如 99,998
console.log(priceNum.toLocaleString());

// 以人民币符号¥加金额的形式展示,例如 ¥99,998.00
console.log(priceNum.toLocaleString("zh", { style: "currency", currency: "CNY" }));

//  展示金额但不展示小数部分,例如 ¥99,998
console.log(priceNum.toLocaleString("zh", { style: "currency", currency: "CNY", minimumFractionDigits: 0 }));

// 以金额加【人民币】的形式展示,例如 99,998.00人民币
console.log(priceNum.toLocaleString("zh", { style: "currency", currency: "CNY", currencyDisplay: "name" }));

// 以全中文的形式展示金额,例如 九九九九八人民币
console.log(priceNum.toLocaleString("zh-Hans-CN-u-nu-hanidec", { style: "currency", useGrouping: false, minimumFractionDigits: 0, currency: "CNY", currencyDisplay: "name" }));

// 展示金额的增减情况,例如 +¥99,998.00
console.log(priceNum.toLocaleString("zh", { style: "currency", currency: "CNY", signDisplay: "always" }));

// 以更人性化的近似方式展示金额,例如 ¥10万
console.log(priceNum.toLocaleString("zh", { style: "currency", currency: "CNY", notation: "compact" }));

按位保留数据

const num = Math.PI;
const num2 = 99.9;

// 默认保留3位小数展示,并保持数据四舍五入,例如 3.142
console.log(num.toLocaleString());

// 默认保留2位小数展示,并保持数据四舍五入,例如 3.14
console.log(num.toLocaleString("zh", { maximumFractionDigits: 2 }));

// 始终展示6位数字且最多保留小数点后3位,例如 003.142
console.log(num.toLocaleString("zh", { minimumIntegerDigits: 3 }));

// 始终展示6位数字且保留小数点后3位,例如 099.900
console.log(num2.toLocaleString("zh", { minimumIntegerDigits: 3, minimumFractionDigits: 3 }));

百分比展示

const num = 0.9982;

// 不保留小数,并保持数据四舍五入,例如 100%
console.log(num.toLocaleString("zh", { style: "percent" }));

// 保留1位小数展示,并保持数据四舍五入,例如 99.8%
console.log(num.toLocaleString("zh", { style: "percent", minimumFractionDigits: 1 }));

// 保留2位小数展示,且展示数据符号,例如 +99.82%
console.log(num.toLocaleString("zh", { style: "percent", minimumFractionDigits: 2, signDisplay: "always" }));

科学计数法

const num = 1123456789;

// 转成科学计数法并保留3位小数,例如 1.123E9
console.log(num.toLocaleString("zh", { notation: "scientific" }));

// 转成科学计数法并保留2位小数,例如 1.12E9
console.log(num.toLocaleString("zh", { notation: "scientific", maximumFractionDigits: 2 }));

// 转成本地化的近似最大单位表示法,例如 11亿
console.log(num.toLocaleString("zh", { notation: "compact" }));

Date.prototype.toLocaleString()

定义

除了 Number.prototype.toLocaleString(),JS中的 Date 对象也提供了同名的方法,对于其的定义这里不再赘述,可以阅读 MDN文档 了解更多,下面会重点介绍其参数与 Number 类型的差异。

Date.prototype.toLocaleString() 同样支持 localesoptions 两个参数,其中 locales 参数的定义与上文基本相同,只是在格式化类型的取值上少了一种,只支持 nuca 两种。

options 参数上,两种类型差别较大,Date 类型支持的属性有以下这些:

参数名称参数类型默认值说明参数示例
localeMatcher"lookup" | "best fit""best fit"使用的 local 匹配算法,详细可以查看 Intl.LocaleMatcher{ localeMatcher: "lookup" }
timeZoneIANA标准的时区运行时的默认时区日期展示所使用的时区{ timeZone: "UTC" }
timeZoneName"short" | "long""short"展示的时区名称样式{ timeZoneName: "long" }
hour12true | false根据 locale 决定,中国为 true是否使用12小时制来展示时间{ hour12: false }
formatMatcher"basic" | "best fit""best fit"格式化的匹配算法,用于从 Date 对象中匹配出对应的年月日时分秒数据,通常不需要关注该属性{ formatMatcher: "basic" }
weekday"narrow" | "short" | "long"undefined工作日的展现方式{ weekday: "narrow" }
era"narrow" | "short" | "long"undefined纪元的展现方式{ era: "narrow" }
year"numeric" | "2-digit"undefined年的展现方式{ year: "2-digit" }
month"numeric" | "2-digit" | "narrow" | "short" | "long"undefined月的展现方式{ month: "2-digit" }
day"numeric" | "2-digit"undefined天的展现方式{ day: "2-digit" }
hour"numeric" | "2-digit"undefined小时的展现方式{ hour: "2-digit" }
minute"numeric" | "2-digit"undefined分钟的展现方式{ minute: "2-digit" }
second"numeric" | "2-digit"undefined秒的展现方式{ second: "2-digit" }

*所有的日期样式的设置,默认值都为 undefined,但是当所有值都为 undefined 时,yearmonthday 的值就会被当做 numeric 处理。 (为什么不直接把这三个的默认值设置为 numeric 嘞 🧐)

常见用法

除了 toLocaleString()Date 对象还提供了很多类似的方法,例如 toLocaleDateString()toLocaleTimeString() 等,感兴趣的同学可以直接查看 MDN文档,这里依据 toLocaleString() 的参数配置,给出部分常见的日期转换用法如下。

const date = new Date(1664379944727);

// 获取完整的24小时时间,例如 2022/9/28 23:45:44
console.log(date.toLocaleString("zh", { hour12: false }));

// 获取四位的年份信息,例如 2022年
console.log(date.toLocaleString("zh", { hour12: false, year: "numeric" }));

// 显示带纪元的年份信息,例如 公元2022年
console.log(date.toLocaleString("zh", { era: "long", year: "numeric"}));

// 以YYYY年MM月DD日HH:mm:ss的形式展示数据,例如 2022年9月28日23:45:44
const a = date.toLocaleString("zh", { year: "numeric", month: "numeric" });
const b = date.toLocaleString("zh", { day: "numeric" });
const c = date.toLocaleString("zh", { hour12: false, hour: "numeric", minute: "numeric", second: "numeric" });
console.log(a + b + c); //  不能直接写在一个配置中就挺离谱

// 获取星期数据,例如 周三
console.log(date.toLocaleString("zh", { weekday: "short" }));

// 获取星期数据,例如 星期三
console.log(date.toLocaleString("zh", { weekday: "long" }));

Array.prototype.toLocaleString()

该方法对于数组整体并没有什么特殊的处理,只是依次对数组中的每个元素都调用了其对应的 toLocaleString() 方法,并将最后的结果字符串数组进行 join 操作,参数 localesoptions 也会同样的传入到每一个数组项的 toLocaleString()

具体效果如下所示:


经过小美的一番教导,小帅终于明白了,原来只需要调用金额数字的 toLocaleString() 方法就可以完美的解决问题,并且还可以通过参数配置使金额展示更加友好。

“真好呀,今天又学习了。”