代码取自一个上线中的房贷计算器核心(纯 TS、零平台依赖),在线DEMO:[房贷计算器](https://jsq3000.com/finance/mortgage)工具可对照结果。
## 金额必须走 Decimal,在边界统一舍入到分
**用 JavaScript 的浮点 `number` 直接算钱一定会出错——`0.1 + 0.2` 在 JS 里等于 `0.30000000000000004`,几百期累加后误差会放大到肉眼可见。** 正确做法是:所有中间计算用高精度的 `decimal.js`,只在输出时统一四舍五入到「分」(2 位小数)。这是房贷、个税、工资这类金融计算的第一条铁律。
封装一个金额工具,把"高精度计算 + 边界舍入"固定下来:
```ts
import Decimal from "decimal.js";
// 中间计算用 40 位精度,输出显式舍入
Decimal.set({ precision: 40, rounding: Decimal.ROUND_HALF_UP });
export function dec(value: Decimal.Value): Decimal {
return new Decimal(value);
}
// 舍入到分(2 位),ROUND_HALF_UP,返回普通 number
export function round2(value: Decimal.Value): number {
return new Decimal(value).toDecimalPlaces(2, Decimal.ROUND_HALF_UP).toNumber();
}
下面两种还款方式都建立在这个基础上。
一、等额本息:每月还固定金额
等额本息(equal installment)指还款期内每月偿还相同金额,其中本金逐月递增、利息逐月递减。 月供 M 的公式是:
M = P · r · (1+r)ⁿ / ((1+r)ⁿ − 1)
其中 P 是贷款本金,r 是月利率(年利率 ÷ 100 ÷ 12),n 是总期数(30 年 = 360 期)。用 decimal.js 实现,注意先处理 r=0(无息)的退化情况避免除零:
const r = dec(annualRatePercent).div(100).div(12);
let monthly: Decimal;
if (r.isZero()) {
monthly = dec(principal).div(n);
} else {
const factor = r.plus(1).pow(n); // (1+r)^n
monthly = dec(principal).mul(r).mul(factor).div(factor.minus(1));
}
const monthlyRounded = round2(monthly);
然后逐期生成还款计划:每期利息 = 当期剩余本金 × 月利率(先舍入到分),本金 = 月供 − 利息:
let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
const interest = dec(round2(remaining.mul(r))); // 当期利息,舍入到分
let principalPart: Decimal, payment: Decimal;
if (period === n) {
principalPart = remaining; // 末期:还清全部剩余本金
payment = principalPart.plus(interest);
} else {
principalPart = dec(monthlyRounded).minus(interest);
payment = dec(monthlyRounded);
}
remaining = remaining.minus(principalPart);
// push 到 schedule...
}
以**贷款 100 万元、年利率 3.5%、30 年(360 期)**为例,这套实现算出:月供约 4490.45 元,还款总额约 1,616,560.07 元,支付利息总额约 616,560.07 元。
二、等额本金:每月还固定本金,月供递减
等额本金(equal principal)指每月偿还相同的本金,利息按剩余本金递减,所以月供逐月减少。 实现更直接:每期本金 = 本金 ÷ 期数(固定),利息 = 剩余本金 × 月利率:
const basePrincipal = round2(dec(principal).div(n));
let remaining = dec(principal);
for (let period = 1; period <= n; period++) {
const interest = dec(round2(remaining.mul(r)));
const principalPart = period === n ? remaining : dec(basePrincipal);
const payment = principalPart.plus(interest);
remaining = remaining.minus(principalPart);
}
同样 100 万 / 3.5% / 30 年:首月月供约 5694.45 元,末月约 2785.08 元,支付利息总额约 526,457.92 元。
对比一下:等额本金比等额本息总共少付利息约 9 万元(616,560 − 526,458 ≈ 90,102 元),代价是前期月供更高、还款压力更大。这就是"提前还得多、总利息少"的直观体现,也是工具要把两种方式并排算给用户看的意义。
三、最关键的工程细节:末期吸收舍入误差
每期利息都舍入到分,几百期累加后必然和理论值产生几分钱的漂移,如果不处理,最后一期会出现"本金没还清"或"多还几分"的 bug。 解决办法是让末期不按公式算本金,而是直接还清当期剩余的全部本金:
if (period === n) {
principalPart = remaining; // 末期 = 剩余本金,吸收所有舍入漂移
payment = principalPart.plus(interest);
}
这样无论前面 359 期累计了多少分的舍入误差,最后一期都会精确把账平掉,保证「逐期本金之和 = 贷款本金」严格成立。这是金融计算里一个朴素但必须的处理——对账要对得上,不能差一分钱。
四、为什么把这套逻辑做成纯函数
计算逻辑写成零依赖的纯 TypeScript 函数(不碰 DOM、不依赖框架),才能同时被 Web、小程序和单元测试复用,也方便和银行口径逐项核对。 输入用强类型约束,并在入口做校验(本金为正、利率非负、期数为正整数):
export interface MortgageInput {
principal: number; // 贷款本金(元)
annualRatePercent: number; // 年利率(%),如 3.5
termMonths: number; // 期数(月),30 年 = 360
method: "equal-installment" | "equal-principal";
}
提前还款(缩短年限 vs 减少月供)也复用同一套月利率和还款计划逻辑,只是把"提前还款时点之后"的剩余本金重新按新期数或新月供再算一遍,再和原计划对比省息。核心仍是上面这套 decimal.js 的精确账。
常见问题(FAQ)
Q:房贷月供怎么算?
等额本息月供用公式 M = P·r·(1+r)ⁿ/((1+r)ⁿ−1),P 为本金、r 为月利率(年利率÷12)、n 为期数。等额本金月供 = 本金÷期数 + 当期剩余本金×月利率,逐月递减。
Q:等额本息和等额本金哪个划算?
等额本金支付的总利息更少。以 100 万、3.5%、30 年为例,等额本息总利息约 61.7 万、等额本金约 52.6 万,等额本金少付约 9 万,但前期月供更高、还款压力更大。怎么选取决于现金流,不是单看利息。
Q:为什么算房贷不能用普通的浮点数?
JavaScript 浮点有精度误差(0.1+0.2=0.30000000000000004),房贷有几百期累加,误差会放大导致月供、利息、剩余本金对不上账。正确做法是用 decimal.js 高精度计算,只在输出时舍入到分。
Q:房贷计算里最后一期为什么要单独处理?
因为每期利息舍入到分会累积几分钱漂移。末期直接偿还剩余的全部本金来吸收误差,保证逐期本金之和精确等于贷款本金,账才对得上。
Q:贷款 100 万、利率 3.5%、30 年,月供和利息是多少?
等额本息月供约 4490.45 元,总利息约 61.66 万元;等额本金首月约 5694.45 元、末月约 2785.08 元,总利息约 52.65 万元。利率或年限变化时需重新计算。
小结
房贷计算的工程要点不在公式本身,而在精度:金额全程走 decimal.js、每期利息舍入到分、末期吸收累计舍入误差保证本金还清;逻辑做成强类型纯函数便于复用和对账。等额本息和等额本金的差别是"利息多少 vs 前期压力大小"的取舍。
想按自己的本金、利率、年限直接算(并对比等额本息/等额本金和提前还款省息),可以用在线版:房贷计算器,输出月供、还款总额、总利息和逐期还款计划。本文与工具仅用于计算方法学习与参考,实际贷款利率、还款方式以银行合同为准,不构成任何投资或借贷建议。
参考来源:等额本息/等额本金为通行还款方式,月供公式为标准年金公式;文中示例(100 万 / 3.5% / 30 年)经 Python decimal 按同一舍入口径复核;实现取自笔者上线的房贷计算核心。利率请以中国人民银行 LPR 及银行实际执行为准。