currency-codes、currency.js、dinero.js 和 numeral 都是 JavaScript 生态中处理数字和货币的常用工具,但它们的定位截然不同。currency-codes 专注于提供全球货币代码、符号和精度的静态数据查询,不进行计算。currency.js 和 dinero.js 是专门的货币计算库,旨在解决浮点数精度问题,其中 dinero.js (v1) 采用不可变链式 API,而 currency.js 提供更轻量的可变或不可变操作。numeral 是一个通用的数字格式化库,用于将数字转换为人类可读的字符串(如添加逗号、百分比),但它不具备货币计算所需的精度安全特性。
在前端开发中,处理金钱是一个高风险领域。JavaScript 原生的 Number 类型基于 IEEE 754 标准,无法精确表示某些小数(例如 0.1 + 0.2 !== 0.3)。为了解决这个问题,社区涌现了多种解决方案。我们将深入对比 currency-codes、currency.js、dinero.js 和 numeral,看看它们各自如何解决不同层面的问题。
这四个库其实分为三类,混用它们会导致严重的架构问题。
currency-codes 是一个数据字典。它不包含任何计算逻辑,只告诉你 "USD" 代表美元,符号是 "$",通常保留 2 位小数。它就像一本字典,你查它获取信息,但它不会帮你算账。
currency.js 和 dinero.js 是计算引擎。它们的核心任务是用整数(Integer)在内部存储货币值(例如存 100 分而不是 1.00 元),从而彻底避开浮点数陷阱。它们提供加减乘除、分配和比较功能。
numeral 是一个格式化器。它的唯一工作是把数字变成好看的字符串。它不关心钱,只关心显示。如果你传给它是 0.1 + 0.2 的结果,它只会漂亮地显示 0.30000000000000004。
这是选择货币库最关键的理由。让我们看看各库如何处理基本的加法运算。
currency.js 允许你像写普通数学一样写代码,它在内部自动处理精度。
import currency from 'currency.js';
const price = currency(10.50);
const tax = currency(2.10);
// 直接相加,返回一个新的 currency 对象
const total = price.add(tax);
console.log(total.value); // 12.60 (精确)
dinero.js 采用更严谨的函数式风格,要求你明确指定货币单位和金额(通常以最小单位,如分为准)。
import dinero from 'dinero.js';
// 必须传入整数(分)和货币代码
const price = dinero({ amount: 1050, currency: 'USD' });
const tax = dinero({ amount: 210, currency: 'USD' });
// 链式调用,返回新对象(不可变)
const total = price.add(tax);
console.log(total.getAmount()); // 1260 (整数分)
numeral 没有计算能力。如果你试图用它处理计算,必须自己先算好(且容易出错)。
import numeral from 'numeral';
// 危险:原生浮点数运算
const rawTotal = 10.50 + 2.10;
// 格式化仅用于显示
const formatted = numeral(rawTotal).format('$0,0.00');
console.log(formatted); // "$12.60" (看似正常,但若遇精度问题则失效)
currency-codes 完全不支持计算。尝试用它做算术会直接报错或毫无反应,因为它只是一个数据对象。
import { getCode } from 'currency-codes';
const data = getCode('USD');
// data.digits 是 2,data.symbol 是 '$'
// 没有 .add() 或 .multiply() 方法
API 的设计直接影响代码的可维护性和安全性。
dinero.js 坚持不可变性(Immutability)。每次操作都返回一个新对象,原对象保持不变。这在复杂的状态管理(如 Redux)中非常安全,能防止副作用,但代码写起来稍显冗长。
// dinero.js: 不可变链式
let myMoney = dinero({ amount: 1000, currency: 'USD' });
let updated = myMoney.add(dinero({ amount: 500, currency: 'USD' }));
// myMoney 仍然是 1000,updated 是 1500
currency.js 更加直观灵活。它虽然也返回新对象,但 API 设计得更像原生数字操作,甚至支持直接与普通数字运算(视配置而定),上手门槛更低。
// currency.js: 直观操作
let myMoney = currency(10.00);
let updated = myMoney.add(5.00); // 可以直接传数字
// 语法糖更多,适合快速开发
numeral 是面向过程的。你传入一个值,调用格式化方法,得到一个字符串。流程结束,没有对象状态需要维护。
// numeral: 过程式格式化
const result = numeral(1234567).format('0,0.00');
// 直接得到字符串 "1,234,567.00"
currency-codes 是纯函数查询。输入代码,输出数据对象。无状态,无副作用。
// currency-codes: 数据查询
const euroData = getCode('EUR');
// 直接获取属性 euroData.symbol
处理全球业务时,你需要知道不同货币的符号、小数位数和代码。
currency-codes 是这方面的专家。它拥有最全面的 ISO 4217 标准数据。如果你需要动态获取 "JPY" 没有小数位,或 "BHD" 有三位小数,它是唯一选择。
import { getCode, getCodes } from 'currency-codes';
// 获取日元信息(0 位小数)
const jpy = getCode('JPY');
console.log(jpy.digits); // 0
console.log(jpy.symbol); // '¥'
// 遍历所有支持的国家
const allCodes = getCodes();
dinero.js 和 currency.js 内部包含基本的货币信息以支持计算(如知道 USD 是 2 位小数),但它们不提供广泛的元数据查询接口。你通常需要将它们与 currency-codes 配合使用,或者手动配置。
// dinero.js 需要你自己传入正确的 amount (整数)
// 它不会告诉你 JPY 不需要小数,你必须自己知道并传入整数
const jpyAmount = dinero({ amount: 100, currency: 'JPY' });
numeral 的国际化依赖于加载特定的语言包(locales),主要用于翻译格式化模板(如逗号和点的位置),但它不懂货币本身的业务规则(如小数位数)。
import numeral from 'numeral';
import 'numeral/locales/locale-de-de';
numeral.locale('de-de');
console.log(numeral(1234.5).format('0,0.00')); // "1.234,50" (德国格式)
在选择库时,必须考虑其生命周期。
numeral 已经不再积极维护。它的 GitHub 仓库多年未更新,存在未修复的 Bug。在现代项目中,建议寻找替代品(如 Intl.NumberFormat 原生 API 或 dayjs 风格的轻量库),除非你只是用它做非常简单的静态格式化且不愿迁移。
dinero.js v1 版本已进入维护模式。团队正在全力开发 v2 版本(重写为 TypeScript,架构更优),但 v2 目前可能仍处于 Alpha/Beta 阶段。对于追求极致稳定的金融项目,v1 依然可用且可靠;对于新项目且不介意早期采用,可以关注 v2 的进展。
currency.js 和 currency-codes 目前保持活跃维护状态,API 稳定,适合大多数生产环境。
| 特性 | currency-codes | currency.js | dinero.js | numeral |
|---|---|---|---|---|
| 核心用途 | 货币元数据查询 | 货币计算 | 高精度货币计算 | 数字格式化 |
| 计算精度 | N/A | ✅ 安全 (整数底层) | ✅ 安全 (整数底层) | ❌ 不安全 (原生 Number) |
| API 风格 | 数据查询 | 直观/轻量 | 函数式/不可变 | 链式格式化 |
| 国际化数据 | ⭐⭐⭐⭐⭐ (最全) | ⭐⭐ (基础) | ⭐⭐ (基础) | ⭐⭐⭐ (格式本地化) |
| 维护状态 | ✅ 活跃 | ✅ 活跃 | ⚠️ v1 维护中/v2 开发 | ❌ 已停滞 |
| 适用场景 | 获取符号/位数 | 快速开发/中小型应用 | 金融级/大型复杂应用 | 仅展示格式化 |
在真实的企业级项目中,我们通常不会只选一个,而是组合使用:
currency-codes 获取当前用户所选货币的元数据(如小数位数)。dinero.js (或 currency.js) 进行所有的金额存储、传输和计算,确保数据绝对准确。numeral,改用浏览器原生的 Intl.NumberFormat 进行最后的格式化显示,因为它性能更好且无需额外依赖,或者在需要兼容旧浏览器时谨慎使用 numeral。// 推荐组合示例
import { getCode } from 'currency-codes';
import dinero from 'dinero.js';
// 1. 查询元数据
const currencyInfo = getCode('USD');
// 2. 安全计算
const amount = dinero({ amount: 1999, currency: 'USD' });
const total = amount.add(dinero({ amount: 500, currency: 'USD' }));
// 3. 原生格式化显示 (替代 numeral)
const display = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(total.getAmount() / Math.pow(10, currencyInfo.digits));
console.log(display); // "$24.99"
不要试图用一个库解决所有问题。如果你的需求是算得准,请在 dinero.js 和 currency.js 之间选择;如果需要查数据,请用 currency-codes;如果只是为了好看,请优先考虑原生 API 而非 numeral。理解它们的边界,才能构建出稳健的金融前端架构。
当你只需要查询货币元数据(如代码 'USD' 对应的符号 '$' 或小数位数)而不涉及任何数学运算时,选择 currency-codes。它是一个纯数据查找表,体积小巧,适合作为其他货币库的补充数据源,用于国际化展示或表单验证。
如果你需要一个轻量级、易于上手的库来处理货币加减乘除,且希望 API 简单直观(支持直接数字运算),currency.js 是不错的选择。它适合中小型项目或快速原型开发,特别是在对不可变性要求不极端严格,但必须避免浮点数错误的场景。
在构建对财务准确性要求极高的大型应用(如银行系统、电商平台结算)时,首选 dinero.js。它的不可变链式 API 能有效防止状态意外修改带来的 Bug,且类型定义完善。注意:目前 v1 版本维护模式,新项目可评估其继任者 Dinero.js v2(alpha 阶段),若需稳定生产环境则 v1 依然可靠。
仅当你需要将已经计算好的数字(无论是普通数字还是货币金额)格式化为特定显示的字符串(如 '1,000.00' 或 '50%')时,才使用 numeral。切勿用它进行任何货币计算,因为它基于 JavaScript 原生 Number 类型,无法避免精度丢失问题。
A node.js module to list and work on currency codes based on the ISO 4217 standard.
npm install currency-codes
var cc = require('currency-codes');
console.log(cc.code('EUR'));
/*
{
code: 'EUR',
number: 978,
digits: 2,
currency: 'Euro',
countries: [
'andorra', 'austria', 'belgium', 'cyprus', 'estonia', 'finland',
'france', 'germany', 'greece', 'ireland', 'italy', 'kosovo',
'luxembourg', 'malta', 'monaco', 'montenegro', 'netherlands',
'portugal', 'san marino', 'slovakia', 'slovenia', 'spain',
'vatican city' ]
}
*/
var cc = require('currency-codes');
console.log(cc.number(967));
/*
{
code: 'ZMW',
number: 967,
digits: 2,
currency: 'Zambian kwacha',
countries: [ 'zambia' ] }
*/
var cc = require('currency-codes');
console.log(cc.country('colombia'));
/*
[
{
code: 'COP',
number: 170,
digits: 2,
currency: 'Colombian peso',
countries: [ 'colombia' ]
}, {
code: 'COU',
number: 970,
digits: 2,
currency: 'Unidad de Valor Real',
countries: [ 'colombia' ]
}
]
*/
var cc = require('currency-codes');
console.log(cc.codes());
/*
[
'AED',
'AFN',
...
'ZAR',
'ZMW'
]
*/
var cc = require('currency-codes');
console.log(cc.numbers());
/*
[
'784',
'971',
...
'710',
'967'
]
*/
var cc = require('currency-codes');
console.log(cc.countries());
/*
[
'united arab emirates',
'afghanistan',
...
]
*/
var data = require('currency-codes/data');
console.log(data);
/*
[{
code: 'AED',
number: '784',
digits: 2,
currency: 'United Arab Emirates dirham',
countries: ['united arab emirates']
}, {
code: 'AFN',
number: '971',
digits: 2,
currency: 'Afghan afghani',
countries: ['afghanistan']
}, {
...
*/
var cc = require('currency-codes');
console.log(cc.publishDate);
/*
2024-06-25
*/
Fetch the latest copy of ISO-4217 from the maintainer and update this library's currency data file.
$ npm run iso
> currency-codes@2.1.0 iso
> npm run iso:fetch-xml && npm run iso:ingest-xml
> currency-codes@2.1.0 iso:fetch-xml
> node scripts/fetch-iso-4217-xml.js
Downloaded https://www.six-group.com/dam/download/financial-information/data-center/iso-currrency/lists/list-one.xml to iso-4217-list-one.xml
> currency-codes@2.1.0 iso:ingest-xml
> node scripts/ingest-iso-4217-xml.js
Ingested iso-4217-list-one.xml into data.js
Wrote publish date to iso-4217-publish-date.js
Note: You may have to manually tweak the capitalization of some country's names.
MIT