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?让它留在历史里吧。仅建议在维护旧项目时使用 numeral,新项目中应避免引入。虽然它支持非常灵活的数字格式化(包括百分比、字节、时间等),但该库已长期停止维护,存在未修复的 bug 且不支持现代 ES 模块标准。如果需要通用数字格式化,建议寻找更新的替代方案或自行编写格式化函数。
对于涉及核心金融逻辑、高并发交易或对数据一致性要求极高的企业级应用,必须选择 dinero.js。它采用不可变数据模式,提供丰富的领域方法(如分配、比较、舍入),并严格遵循金融计算标准。虽然学习曲线较陡且包体积较大,但它能从根本上杜绝精度丢失和状态突变带来的 bug,是构建可靠财务系统的首选。
当你需要在浏览器端进行简单的货币加减乘除运算,且希望避免 JavaScript 原生浮点数精度错误(如 0.1 + 0.2 !== 0.3)时,应选择 currency.js。它提供了类似 jQuery 的链式调用 API,上手成本低,适合中小型项目或原型开发。不过,它的功能集相对基础,不适合处理复杂的税务计算或货币兑换逻辑。
如果你的需求仅仅是将数字转换为符合特定地区标准的货币字符串(如 '$1,234.56'),而不涉及复杂的数学运算,currency-formatter 是最佳选择。它轻量且专注于格式化,API 简单直接,适合用于电商列表页、报表展示等只读场景。但请注意,它不负责解决浮点数精度问题,计算逻辑需自行处理。
A javascript library for formatting and manipulating numbers.
#CDNJS
develop branch. All pull requests must include the appropriate tests.Fork the library
Run npm install to install dependencies
Create a new branch from develop
Add your tests to the files in /tests
To test your tests, run grunt
When all your tests are passing, run grunt dist to compile and minify all files
Submit a pull request to the develop branch.
Formats now exist in their own files and act more or less as plugins. Check out the bytes format for an example of how to create one.
When naming locale files use the ISO 639-1 language codes supplemented by ISO 3166-1 country codes when necessary.
See the english unit tests for an example.
Bug fix: Multi letter currency symbols and spacing
Added: Formatting of numbers with leading zeros
New format: Basic Point
Option: Added scalePercentBy100 (default: true) option to turn on/off scaling percentages
Bug fix: Incorrect abbreviations for values rounded up #187
Bug fix: Signed currency is inconsistent #89
Bug fix: Updated module definitions
Bug fix: Fixed regression for webpack/browserify/rollup
2.0.0 brings a lot of breaking changes and a reorganization of the repo, but also simplifies the api as well as the creating of custom formats.
Breaking change / Feature: All formats are now separate files. This makes it easy to create custom formats, and will also allow for custom builds with only certain formats. (Note: The built numeral.js still contains all formats in the repo).
Breaking change / Feature: All formats and locales are now loaded using numeral.register(type, name, {})
Breaking change: All language now renamed to locale and standardized to all lowercase filenames
Breaking change: The locale function no longer loads locales, it only sets the current locale
Breaking change: The unformat function has been removed numeral().unformat(string) and now happens on numeral init numeral(string)
Breaking change / Feature: Bytes are now formatted as: b (base 1000) and ib (base 1024)
Breaking change: numeral(NaN) is now treated the same as numeral(null) and no longer throws an error
Feature: Exponential format using e+ or e-
Bug fix: Update to floating point helpers (Note: Numeral does not fix JS floating point errors, but look to our tests to see that it covers quite a few cases.)
Bug fix: numeral converts strings to numbers
Bug fix: Null values return same as 0
Contained breaking changes, recommended to use 1.5.6
Bug fix: Switch bytes back to b and change iecBinary to ib, and calculate both using 1024 for backwards compatibility
Contained breaking changes, recommended to use 1.5.6
Tests: Changed all tests to use Mocha and Chai
Tests: Added browser tests for Chrome, Firefox, and IE using saucelabs
Added reset function to reset numeral to default options
Added nullFormat option
Update reduce polyfill
Added Binary bytes
Bug fix: Fixes problem with many optional decimals
Added currency symbol to optionally appear before negative sign / open paren
Added float precision math support
Added specification of abbreviation in thousands, millions, billions
Bug fix: Unformat should pass through if given a number
Added a mechanism to control rounding behaviour
Added languageData() for getting and setting language props at runtime
Bug fix: Make sure values aren't changed during formatting
Add defaultFormat(). numeral().format() uses the default to format if no string is provided
.unformat() returns 0 when passed no string
Added languages.js that contains all languages
Bug fix: Fix bug while unformatting ordinals
Add format option to always show signed value
Added ability to instantiate numeral with a string value of a number
Bug fix: Fix bug while unformatting ordinals
Bug fix: Throw error if language is not defined
Bug fix: Fix typo for trillion
Bug fix: remove ' from unformatting regex that was causing an error with fr-ch.js
Add zeroFormat() function that accepts a string for custom formating of zeros
Add valueOf() function
Chain functionality to language function
Make all minified files have the same .min.js filename ending
Bug fix: Bytes not formatting correctly
Add optional format for all decimals
Remove AMD module id. (This is encouraged by require.js to make the module more portable, and keep it from creating a global)
AMD define() compatibility.
Bug fix: Formatting some numbers results in the wrong value. Issue #21
Bug fix: Minor fix to unformatting parser
Add support for spaces before/after $, a, o, b in a format string
Bug fix: Fix unformat for languages that use '.' in ordinals
Bug fix: Fix round up floating numbers with no precision correctly.
Bug fix: Fix currency signs at the end in unformat
Add support for optional decimal places
Add support for appending currency symbol
Add support for humanized filesizes
Bug Fix: Fix unformatting for languages that use '.' as thousands delimiter
Changed language definition property 'money' to 'currency'
Bug fix: Fix unformatting non-negative abbreviations
Add language support
Update testing for to include languages
Add Tests
Bug fix: Fix difference returning negative values
Bug fix: Non negative numbers were displaying as negative when using parentheses
Add ordinal formatting using 'o' in the format
Add clone functionality
Added abbreviations for thousands and millions using 'a' in the format
Initial release
Numeral.js, while less complex, was inspired by and heavily borrowed from Moment.js
Numeral.js is freely distributable under the terms of the MIT license.
Copyright (c) 2012 Adam Draper
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.