我正在参加「掘金·启航计划」
我有一个朋友叫小帅,是位前端开发工程师。
这天,小帅在开发一个数据展示的功能时遇到了点小问题。
产品希望将表格中金额数字展示为逗号分隔的形式,例如123456
展示为123,456
。
小帅简单🤔了一下,手动去造一个数字处理的轮子属实没必要,但是为了这么个小需求去引入第三方库好像也有点说不过去,于是小帅开始面露难色,有没有比较简单的又不用引入第三方库的方法呢?
此时,旁边的小美悠悠地来了一句:难道你不知道JS中有个方法是toLocaleString吗❓
Number.prototype.toLocaleString()
定义
toLocaleString()
方法返回这个数字在特定语言环境下的表示字符串。
MDN 上的定义永远都是那么简单,然而越简单就越难完全理解其中的含义。将数字转换为字符串这个很容易理解,那么特定语言环境下是什么环境呢,它跟 toString()
方法有什么区别呢❓
实践是检验真理的唯一标准。
让我们打开 Chrome
的开发者工具,修改浏览器的 locale
,操作流程如下所示:
- 通过
F12
或者Command + Option + i
打开浏览器的开发者工具; - 按下
ESC
唤起开发者工具控制台的隐藏抽屉; - 在隐藏抽屉 Tab 栏选择
传感器(英文叫 sensor)
进入位置设置面板; - 下拉选择并替换
位置(英文叫 Location)
中选项; - 在控制台中调用待测试的
toLocaleString()
代码并查看输出结果。
在不同的 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 中新增了 locales
和 options
这两个参数的支持,允许用户进行指定要进行格式转换的语言并定制转换函数的行为。
目前主流浏览器都是支持
locales
和options
这两个参数的,所以在项目中可以放心使用。
使用方法如下所示:
MDN 中对于参数 locales
和 options
的描述相当复杂,这里不再赘述,只对其中模棱两可的地方以及关键知识点做简单的总结。
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(使用中国地区简体中文的十进制方式格式化数据) |
格式化类型 | Collator,NumberFormat,或者 DateTimeFormat 对象对应的标签值(co 、nu 、ca ) | 必须在 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" } |
currency | ISO 中的货币代码 | 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" } |
useGrouping | true | false | true | 是否使用分组分隔符,如千位分隔符, 或其他千/万/亿分隔符 | { 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" } |
minimumIntegerDigits | number | 1 | 默认使用的整数数字的最小位数,不够的话会自动补 0 ,范围为 1 - 21 | { minimumIntegerDigits: 21 } |
minimumFractionDigits | number | 0 | 默认使用的小数数字的最小位数,不够的话自动补 0 ,范围为 0 - 20 | { minimumIntegerDigits: 20 } |
maximumFractionDigits | number | 3 | 默认使用的小数数字的最大位数,取值范围为 0 - 20 ,纯数字格式的默认值是和3 中大的那一个;货币格式默认值是和 ISO 4217 currency code list 中大的那一个 (如果列表中没有提供则值为 2);和0 中大的那一个 | { maximumFractionDigits: 20 } |
minimumSignificantDigits | number | 1 | 使用的有效数字的最小位数,取值范围为 1 - 21 | { minimumSignificantDigits: 21 } |
maximumSignificantDigits | number | 21 | 使用的有效数字的最大位数,取值范围为 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()
同样支持 locales
和 options
两个参数,其中 locales
参数的定义与上文基本相同,只是在格式化类型
的取值上少了一种,只支持 nu
和 ca
两种。
在 options
参数上,两种类型差别较大,Date
类型支持的属性有以下这些:
参数名称 | 参数类型 | 默认值 | 说明 | 参数示例 |
---|---|---|---|---|
localeMatcher | "lookup" | "best fit" | "best fit" | 使用的 local 匹配算法,详细可以查看 Intl.LocaleMatcher | { localeMatcher: "lookup" } |
timeZone | IANA标准的时区 | 运行时的默认时区 | 日期展示所使用的时区 | { timeZone: "UTC" } |
timeZoneName | "short" | "long" | "short" | 展示的时区名称样式 | { timeZoneName: "long" } |
hour12 | true | 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
时,year
、month
、day
的值就会被当做 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
操作,参数 locales
和 options
也会同样的传入到每一个数组项的 toLocaleString()
中。
具体效果如下所示:
经过小美的一番教导,小帅终于明白了,原来只需要调用金额数字的 toLocaleString()
方法就可以完美的解决问题,并且还可以通过参数配置使金额展示更加友好。
“真好呀,今天又学习了。”