房贷计算器里最容易被低估的部分,不是月供公式,而是“逐期还款计划”。
等额本息和等额本金的总利息差异,表面上看是两个公式的区别;真正写进代码后,还会碰到期数换算、零利率、最后一期尾差,以及金额保留两位小数后“本金、利息、月供加不回来”等问题。
本文以 等额本息与等额本金对比工具 为例,拆解这类工具的计算逻辑。
先统一输入口径
不管是等额本息还是等额本金,输入都应先转换成统一单位:
const principal = amountInWan * 10_000; // 元
const months = years * 12; // 期
const monthlyRate = annualRate / 12; // 月利率
这里的 annualRate 应是小数,例如年利率 4% 传入 0.04,而不是 4。
工具页面应明确这一点。用户填写“4”通常表示 4%,程序内部却必须使用 0.04。这类单位错误很隐蔽,算出来的数也“像是真的”,但会大得离谱。
等额本息:每月月供固定,本金占比逐步上升
设:
- 贷款本金为
P - 月利率为
r - 总期数为
n - 每期月供为
A
等额本息的月供公式为:
[
A = \frac{P \times r \times (1+r)^n}{(1+r)^n - 1}
]
每一期先根据剩余本金计算利息:
[
interestk = balance{k-1} \times r
]
再由固定月供倒推当期归还的本金:
[
principal_k = A - interest_k
]
剩余本金随之更新:
[
balancek = balance{k-1} - principal_k
]
实现时不必单独推导每一期的封闭公式,按还款顺序循环更容易检查:
let balance = principal;
for (let period = 1; period <= months; period += 1) {
const interest = balance * monthlyRate;
const principalPaid = payment - interest;
balance -= principalPaid;
schedule.push({
period,
payment,
principal: principalPaid,
interest,
balance,
});
}
以“贷款 100 万元、年利率 4%、期限 30 年”为例,等额本息月供约为 4774.15 元。第一期利息约 3333.33 元,本金只还了约 1440.82 元。前几年看起来还得不少,但剩余本金下降得并不快。
等额本金:每期本金固定,月供逐月下降
等额本金更直接。每期归还的本金固定:
[
principal_k = \frac{P}{n}
]
第 k 期的利息按期初余额计算:
[
interest_k = \left(P - (k-1) \times \frac{P}{n}\right) \times r
]
所以当期月供是:
[
payment_k = principal_k + interest_k
]
对应代码:
const principalPerMonth = principal / months;
let balance = principal;
for (let period = 1; period <= months; period += 1) {
const interest = balance * monthlyRate;
const payment = principalPerMonth + interest;
balance -= principalPerMonth;
schedule.push({
period,
payment,
principal: principalPerMonth,
interest,
balance: Math.max(0, balance),
});
}
还是上面的 100 万、4%、30 年案例,等额本金首月约还 6111.11 元,最后一期约 2787.04 元;总利息约 60.17 万元。等额本息的总利息约为 71.87 万元,两者相差约 11.7 万元。
差异不在于某一种方式“更划算”这么简单。等额本金把更多本金放在前期偿还,因此前期月供更高,也更早减少了后续计息的本金基数。
总利息可以用逐期计划汇总,不要只依赖单个公式
等额本金的总利息可以用等差数列快速校验:
[
totalInterest =
P \times r \times \frac{n+1}{2}
]
但实际开发中,仍建议两种方式都从逐期还款计划累加得出总利息:
const totalInterest = schedule.reduce(
(sum, row) => sum + row.interest,
0,
);
const totalPayment = schedule.reduce(
(sum, row) => sum + row.payment,
0,
);
原因很实际:后续如果要支持提前还款、利率调整、每月服务费,或者某一期有额外还款,单一总公式很快就不够用了;逐期计划仍然可以继续复用。
舍入不要参与核心计算
金额展示通常保留两位小数:
const money = (value: number) =>
Math.round((value + Number.EPSILON) * 100) / 100;
但有一个常见坑:每期刚算完就把本金、利息、月供全部截断到分,然后再用这些截断值继续算下一期。30 年共 360 期,尾差很容易积累,最后可能出现:
- 最后一期剩余本金不是 0;
- 本金合计不是贷款本金;
- 月供合计不等于本金加利息。
更稳妥的做法是:
- 内部使用未舍入的浮点数或整数分进行计算;
- 仅在展示每一期时保留两位小数;
- 最后一期用剩余本金兜底。
if (period === months || principalPaid > balance) {
principalPaid = balance;
payment = principalPaid + interest;
}
这样即使前面存在浮点数误差,也能保证最后一期把剩余本金归零。
如果业务要求每一期扣款金额都精确到“分”,还应在生成计划后做一次尾差校正:把前 n - 1 期按规则舍入,最后一期用“应还本金余额 + 当期利息”计算。不能只把最后一期显示成 0,却让累计本金仍对不上。
零利率不是异常数据
月供公式的分母包含:
[
(1+r)^n - 1
]
当 r = 0 时分母为 0,直接套公式会得到 NaN 或 Infinity。零利率虽然少见,但它是合法输入,应该单独处理:
const payment =
monthlyRate === 0
? principal / months
: (principal * monthlyRate * Math.pow(1 + monthlyRate, months)) /
(Math.pow(1 + monthlyRate, months) - 1);
对应结果应为:每期归还相同本金,总利息为 0。
对比工具不该只展示“谁省利息”
一个实用的对比页至少要同时呈现:
- 等额本息的固定月供;
- 等额本金的首月月供和末月月供;
- 两种方式的总利息;
- 两种方式的逐期本金、利息、剩余本金;
- 首月压力差额。
因为“等额本金少付多少利息”容易理解,“首月多还多少”更接近用户的现金流现实。
我在实现这类页面时,更倾向于先生成两套独立计划,再从计划里派生指标,而不是先算几个汇总数字。这样后续加图表、查第 60 期剩余本金、模拟提前还款,都不需要重写底层逻辑。
可以直接使用 等额本息与等额本金对比工具,在同一本金、利率和期限下查看两套还款计划。实际贷款以合同、放款日、计息规则和银行结算结果为准。