currency-formatter、currency.js、dinero.js 和 numeral 都是用于处理数字格式化和货币计算的 JavaScript 库,但它们的侧重点截然不同。numeral 是一个通用的数字格式化工具,支持多种格式但已停止维护;currency-formatter 专注于根据 ISO 标准快速生成货币字符串;currency.js 旨在解决浮点数精度问题,提供轻量级的货币计算能力;而 dinero.js 则是一个功能完备的不可变货币对象库,专为复杂的金融逻辑和高精度计算设计。开发者需要根据项目是仅需展示格式化、需要简单计算,还是需要严格的金融级精度和领域模型来选择相应的工具。
在前端开发中,处理货币不仅仅是加个美元符号那么简单。我们面临着三个核心挑战:本地化格式化(不同国家的符号和分隔符)、浮点数精度陷阱(JavaScript 原生 Number 类型的致命缺陷)以及业务逻辑复杂性(舍入规则、货币分配)。
currency-formatter、currency.js、dinero.js 和 numeral 代表了四种不同的解决思路。本文将深入剖析它们的内部机制,通过代码实战帮你做出正确的架构决策。
numeral 的状态在深入技术细节前,必须明确一点:numeral 已不再维护。它的最后一次更新远在多年前,社区已普遍转向其他方案。虽然它的 API 设计曾经非常优雅,支持极其丰富的格式化模板,但在现代技术栈中引入一个“死库”会带来安全隐患和构建工具兼容性问题。
// numeral (已过时,不推荐在新项目使用)
// 这种写法在现代打包工具中可能需要额外的 shim 配置
var numeral = require('numeral');
numeral(1000.234).format('$0,0.00'); // 输出:$1,000.23
建议:如果你的项目仅需格式化,请迁移至 currency-formatter 或原生 Intl.NumberFormat;若需计算,请选择 currency.js 或 dinero.js。
当你的需求只是将后端传来的数字(通常是整数分或浮点数)展示给用户看时,性能和解耦是关键。
currency-formatter:专注格式的轻量选手currency-formatter 不做任何计算,它只做一件事:把数字变成字符串。它依赖 ISO 4217 标准数据,能正确处理各种奇怪的货币符号位置和小数位数(如日元没有小数位)。
import { format } from 'currency-formatter';
// 自动处理日元无小数位
format(5000, { code: 'JPY' });
// 输出:"¥5,000"
// 自定义欧元格式
format(1234.56, { code: 'EUR', locale: 'de-DE' });
// 输出:"1.234,56 €" (符合德国习惯)
优势:体积极小,无副作用,专门解决 Intl.NumberFormat 在某些旧环境或特定配置下不够直观的问题。
劣势:无法处理计算,输入必须是数字,如果传入浮点数进行复杂运算后再格式化,精度问题依然存在。
numeral (历史参考)作为对比,看看 numeral 曾经如何处理。它的模板系统非常强大,但过于灵活导致容易出错。
// numeral (已过时)
numeral(1234567).format('0,0.00 $'); // 完全自定义模板
// 输出:"1,234,567.00 $"
对比结论:在现代项目中,currency-formatter 比 numeral 更安全、更专注。如果只需要格式化,甚至可以直接使用浏览器原生的 Intl.NumberFormat,无需额外依赖。
当需要在购物车、表单预览等场景进行简单的加减乘除,且必须避免 0.1 + 0.2 = 0.30000000000000004 这种尴尬时。
currency.js:以开发者体验为导向currency.js 的核心思想是“让货币像数字一样好用,但没有精度问题”。它在内部将金额转换为整数(分)进行计算,但在 API 层面暴露为浮点数对象,支持链式调用。
import currency from 'currency.js';
const price = currency(10.50);
const tax = currency(2.10);
// 链式计算,自动处理精度
const total = price.add(tax).multiply(2).subtract(1);
console.log(total.value); // 获取原始数字 (24.00)
console.log(total.format()); // 直接格式化 "$24.00"
技术细节:
currency("10.50")。适用场景:电商购物车、简单的费用计算器、SaaS 订阅价格预览。 局限性:不支持复杂的金融规则(如银行家舍入法的高级配置),也不支持货币兑换逻辑。
当你的应用涉及账务系统、多方金额分配、复杂的税务逻辑或需要严格审计时,简单的库已经不够用了。你需要的是一个“领域模型”。
dinero.js:不可变的货币对象dinero.js 不仅仅是一个工具库,它是一个完整的货币领域模型。它强制你使用“分”或最小货币单位作为整数输入,从根本上杜绝了浮点数。它的所有操作都是不可变的(Immutability),即每次操作都会返回一个新的 Dinero 对象,而不是修改原值。
import Dinero from 'dinero.js';
// 必须使用整数(美分)初始化,杜绝浮点歧义
const price = Dinero({ amount: 1050, currency: 'USD' }); // $10.50
const tax = Dinero({ amount: 210, currency: 'USD' }); // $2.10
// 计算返回新对象,原对象不变
const total = price.add(tax);
// 复杂的舍入控制
const halfDollar = Dinero({ amount: 1235, currency: 'USD' });
const rounded = halfDollar.toUnit('dollar', { round: 'HALF_UP' });
// 货币分配(解决分钱除不尽的问题)
// 将 $10.03 分配给 3 个人,不会出现丢失 1 分钱的情况
const [alice, bob, charlie] = Dinero({ amount: 1003, currency: 'USD' }).allocate([33, 33, 34]);
核心优势:
allocate(分配)、compare(比较)、hasSameCurrency(同币种校验)等金融专用方法。代价:
10.5。currency.js 大得多。这是最本质的区别。currency.js 试图对你输入的浮点数进行“补救”,而 dinero.js 直接禁止你输入浮点数。
// currency.js: 尝试修复浮点数,但输入仍是浮点
const c1 = currency(0.1).add(0.2);
// 内部转换:10 分 + 20 分 = 30 分 -> 0.3 (成功)
// dinero.js: 强制整数,从源头消灭问题
const d1 = Dinero({ amount: 10, currency: 'USD' }).add(
Dinero({ amount: 20, currency: 'USD' })
);
// 必须显式构造,无法误传 0.1
分析:在简单场景下,currency.js 的 DX(开发体验)更好;但在涉及大量数据导入或用户自由输入的场景,dinero.js 的严格模式能防止更多潜在 Bug。
在金融场景中,将总金额按比例分配给多方时,常因除不尽导致“丢失几分钱”。这是检验库专业度的试金石。
// dinero.js: 内置专业的分配算法
const amounts = Dinero({ amount: 100, currency: 'USD' }).allocate([50, 50]);
// 结果:[50, 50]
const amounts2 = Dinero({ amount: 101, currency: 'USD' }).allocate([50, 50]);
// 结果:[51, 50] (自动处理余数,保证总和不变)
// currency.js: 无此功能,需手动实现
// 开发者必须自己写逻辑处理余数,极易出错
const total = 101;
const part1 = Math.floor(total * 0.5);
const part2 = total - part1; // 手动补丁
结论:只要涉及“分账”、“佣金计算”、“税费分摊”,dinero.js 是唯一可靠的选择。
// currency.js: 可变操作 (部分方法) 或 返回新对象混合
// 需注意链式调用是否改变了原引用,文档虽声称返回新对象,但内部状态管理较简单
const cart = currency(100);
const updated = cart.add(10);
// dinero.js: 严格不可变
const original = Dinero({ amount: 100, currency: 'USD' });
const modified = original.add(10);
console.log(original.getAmount()); // 依然是 100,绝对安全
在 React 或 Vue 等响应式框架中,不可变数据能显著减少调试难度,确保状态流转清晰。
| 特性 | currency-formatter | currency.js | dinero.js | numeral |
|---|---|---|---|---|
| 主要用途 | 纯格式化展示 | 轻量级计算 + 格式化 | 金融级计算 + 领域模型 | 通用格式化 (已过时) |
| 精度处理 | 无 (依赖输入) | 内部转整数,对外屏蔽 | 强制整数输入,严格 | 无 (浮点数) |
| API 风格 | 函数式 | 链式 (jQuery 风) | 链式 (函数式风) | 链式 |
| 不可变性 | N/A | 部分 | 严格不可变 | 否 |
| 高级功能 | 多 Locale 支持 | 基础四则运算 | 分配、比较、兑换、舍入策略 | 丰富但过时 |
| 维护状态 | ✅ 活跃 | ✅ 活跃 | ✅ 活跃 (v2 重构中) | ❌ 停止维护 |
| 推荐场景 | 商品列表、报表 | 购物车、表单 | 账务系统、支付核心 | 无 (请避免) |
对于绝大多数内容型网站或简单电商:
组合使用 原生 Intl.NumberFormat (用于格式化) + 简单的整数运算 (后端返回分,前端直接加减)。如果不想处理整数转换,使用 currency.js 是最快的落地方案,它能以最小的成本解决精度痛点。
对于 Fintech、SaaS 计费、复杂 ERP 系统:
不要犹豫,直接上 dinero.js。虽然前期需要适应“整数输入”和“不可变对象”的模式,但它提供的 allocate 方法和严格的类型约束,会在项目后期节省大量的调试时间和资金风险。它的代码即文档,明确表达了货币的领域逻辑。
关于 numeral 的迁移:
如果老项目中使用了 numeral,建议制定计划逐步替换。格式化逻辑可替换为 currency-formatter 或原生 API;计算逻辑必须重构为 currency.js 或 dinero.js,否则精度 Bug 随时可能爆发。
最后总结:
currency-formatter。currency.js。dinero.js。numeral?让它留在历史里吧。如果你的需求仅仅是将数字转换为符合特定地区标准的货币字符串(如 '$1,234.56'),而不涉及复杂的数学运算,currency-formatter 是最佳选择。它轻量且专注于格式化,API 简单直接,适合用于电商列表页、报表展示等只读场景。但请注意,它不负责解决浮点数精度问题,计算逻辑需自行处理。
当你需要在浏览器端进行简单的货币加减乘除运算,且希望避免 JavaScript 原生浮点数精度错误(如 0.1 + 0.2 !== 0.3)时,应选择 currency.js。它提供了类似 jQuery 的链式调用 API,上手成本低,适合中小型项目或原型开发。不过,它的功能集相对基础,不适合处理复杂的税务计算或货币兑换逻辑。
对于涉及核心金融逻辑、高并发交易或对数据一致性要求极高的企业级应用,必须选择 dinero.js。它采用不可变数据模式,提供丰富的领域方法(如分配、比较、舍入),并严格遵循金融计算标准。虽然学习曲线较陡且包体积较大,但它能从根本上杜绝精度丢失和状态突变带来的 bug,是构建可靠财务系统的首选。
仅建议在维护旧项目时使用 numeral,新项目中应避免引入。虽然它支持非常灵活的数字格式化(包括百分比、字节、时间等),但该库已长期停止维护,存在未修复的 bug 且不支持现代 ES 模块标准。如果需要通用数字格式化,建议寻找更新的替代方案或自行编写格式化函数。
A simple Javascript utility that helps you to display currency properly
Please don't add another dependency which you don't need. All modern browsers (and node.js) have this functionality built-in and do a much better job at formatting currencies. e.g. #57
Example:
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(100000000)
// => "$100,000,000.00"
new Intl.NumberFormat('en-US', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "€100,000,000.00"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'USD' }).format(100000000)
// => "100.000.000,00 $"
new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "100.000.000,00 €"
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(100000000)
// => "100 000 000,00 €"
With that being said use this library if you need:
full-icu. See: #72 and #19214npm install currency-formatter --save
By specifying the currency code
var currencyFormatter = require('currency-formatter');
currencyFormatter.format(1000000, { code: 'USD' });
// => '$1,000,000.00'
currencyFormatter.format(1000000, { code: 'GBP' });
// => '£1,000,000.00'
currencyFormatter.format(1000000, { code: 'EUR' });
// => '1 000 000,00 €'
Or by specifying the locale
var currencyFormatter = require('currency-formatter');
currencyFormatter.format(1000000, { locale: 'en-US' });
// => '$1,000,000.00'
currencyFormatter.format(1000000, { locale: 'en-GB' });
// => '£1,000,000.00'
currencyFormatter.format(1000000, { locale: 'GB' });
// => '£1,000,000.00'
currencyFormatter.format(1000000, { locale: 'de-DE' });
// => '1.000.000,00 €'
currencyFormatter.format(1000000, { locale: 'nl-NL' });
// => '€1.000.000,00'
You can also get the currency information.
var currencyFormatter = require('currency-formatter');
currencyFormatter.findCurrency('USD');
// returns:
// {
// code: 'USD',
// symbol: '$',
// thousandsSeparator: ',',
// decimalSeparator: '.',
// symbolOnLeft: true,
// spaceBetweenAmountAndSymbol: false,
// decimalDigits: 2
// }
Parse the number of a monetary value
currencyFormatter.unformat('$10.5', { code: 'USD' })
// => 10.5
currencyFormatter.unformat('$1,000,000', { code: 'USD' })
// => 1000000
currencyFormatter.unformat('10,5 €', { code: 'EUR' })
// => 10.5
currencyFormatter.unformat('1 000 000,00 €', { code: 'EUR' })
// => 1000000
currencyFormatter.unformat('1.000,99', { locale: 'de-DE' })
// => 1000.99
currencyFormatter.unformat('10\'000 CHF', { code: 'CHF' })
// => 10000
currencyFormatter.unformat('10.00 CHF', { code: 'CHF' })
// => 10
currencyFormatter.unformat('10,00 CHF', { code: 'CHF' })
// => 1000
Currency Formatter uses accounting library under the hood, and you can use its options to override the default behavior.
var currencyFormatter = require('currency-formatter');
currencyFormatter.format(1000000, {
symbol: '@',
decimal: '*',
thousand: '^',
precision: 1,
format: '%v %s' // %s is the symbol and %v is the value
});
// => '1^000^000*0 @'
// Different formatting for positive and negative values
currencyFormatter.format(-10, {
format: {
pos: '%s%v' // %s is the symbol and %v is the value
neg: '(%s%v)',
zero: '%s%v'
}
});
// => ($10)
You could also get a list of all the currencies here using one of the following:
var currencies = require('currency-formatter/currencies');
// OR
var currencyFormatter = require('currency-formatter');
var currencies = currencyFormatter.currencies;
Or the currencies in hashmap shape:
var currencies = require('currency-formatter/currencies.json');
// Result:
// {
// "USD": {
// "code": "USD",
// "symbol": "$",
// "thousandsSeparator": ",",
// "decimalSeparator": ".",
// "symbolOnLeft": true,
// "spaceBetweenAmountAndSymbol": false,
// "decimalDigits": 2
// },
// ...more currencies
// }