房贷、利息、百分比三台计算器:页面上没写出来的那些规则
房贷计算器、利息计算器、百分比计算器 三台机器挨在一起,但它们的写法差异大到足以让你在某一页养成的习惯在下一页失效。这篇就是它们的行为说明:每台算的是什么、填了荒唐东西会怎样,以及哪些显示出来的数字并不是它看上去那个意思。下面每个数字都是把页面跑起来测的,不是抄说明书。
三台机器,三种脾气
- 房贷页只回答一个问题——固定利率的分期贷款要花多少钱——而且每敲一个字就重算一次。
- 利息页在三个标签后面藏了三种互不相同的模型(单利、复利、按月定投),只有你按下按钮它才计算。
- 百分比页回答四个一句话的问题,完全没有时间维度,并且把得出答案的那行算式打在答案旁边。
它们对「钱」的态度也不一致。房贷页的金额按万元填(填 100 就是 1,000,000),输出带 ¥ 符号、表头写着「元」;利息页三处输入的单位都写「元」,导出的 CSV 表头也写「元」;百分比页只有「原价」那一栏带「元」,其余全是无单位数字——所以它的任何一个结果,都可能在两边输入的单位不一致时安静地错掉。
三台机器的读数方式完全相同:parseFloat(输入框.value) || 0。这一个表达式决定了大部分边界行为,因为 parseFloat 遇到读不懂的内容会返回 NaN,而 || 0 又把它变成 0。空着、填 abc、填 12abc(会当成 12)——全都接受,全都不报错。
房贷:标题那个数是怎么来的
按页面默认值——金额栏 100(即 100 万)、30 年、年利率 3.85%——等额本息给结果是:
| 位置 | 数值 |
|---|---|
| 月供金额 | 4,688 |
| 还款总额 (元) | 1,687,710 |
| 利息总额 (元) | 687,710 |
| 利息占比 | 40.7% |
算法就是标准的年金公式:期数 = 年 × 12,月利率 = 年利率 ÷ 100 ÷ 12。这是名义利率口径,等于假定报价利率按月复利、每月吃十二分之一。如果对方给你的是一个已经把费用揉进利率里的 APR,这里对不上。总额是用未取整的月供算出来的,不是把上面那个 4,688 乘以 360。
要注意「利息占比」这一格的分母:脚本算的是 利息总额 ÷ 还款总额,也就是 687,710 ÷ 1,687,710 = 40.7%。另一个常见口径「利息是本金的几成」是 687,710 ÷ 1,000,000 = 68.8%,页面上没有任何一格显示这个数。
同一笔贷款换个还法,少付 108,606
同样默认值下切到等额本金,四个标题数字全变:
| 还款方式 | 首月 / 固定月供 | 还款总额 | 利息总额 | 利息占比 |
|---|---|---|---|---|
| 等额本息 · 固定月供 | 每月 4,688 | 1,687,710 | 687,710 | 40.7% |
| 等额本金 · 递减还款 | 首月 5,986 | 1,579,104 | 579,104 | 36.7% |
等额本金还会在标题下面多出一行:首月月供 5,986 元、每月递减 8.91 元。这个递减额是 本金 ÷ 期数 × 月利率,在这个模型里是精确值——也正是它总利息更低的原因:本金更早地变小,而利息是按余额收的。
两种方式是同一个变量的两个状态,除了原始输入之外,你的任何设置都不会在两者之间传递。
0% 算不了,尽管代码为此准备过一条分支
脚本里确实有「月利率为 0」的分支:r === 0 时月供直接取 P / n。你走不到那里。在任何数学之前有一道闸门:金额不大于 0、年限小于 1、年利率不大于 0,这三种情况一律拒绝计算。实测填 0 年利率时,四个结果位显示破折号,副标题变成「请填写完整参数」,等额本金那一行隐藏,明细表清空。免息贷款是代码准备好了、界面不肯给的那一种情形。
明细表加不出总额,差三块钱上下
表格每一格都过一遍 Math.round(value).toLocaleString(),也就是逐行独立四舍五入到元;合计却是用精确值累加的。于是列加起来对不上合计行。默认值下实测:
- 按年视图(31 行:30 年 + 一行「合计」):本金列加总为 1,000,003,而合计行写的是 1,000,000。
- 按月视图(360 行):本金列加总为 999,997。按月视图根本没有合计行——360 行期次后面什么都不剩,表格自身没有可对照的总额,只有上面那几个标题数字。
这个差额是舍入痕迹,不是算错,而且随你累加的行数增长。真需要能加平的列,用精确月供 4,688.0823…(×360 = 1,687,709.64)去排,别用显示出来的那个 4,688。
剩余本金那一列也被夹过——Math.max(0, remaining)——所以最后一行不可能出现负余额,代价是末期那几分钱的残差被藏起来了,而不是显示出来。
源码里已经写了一份答案,而且是旧的
把脚本关掉再打开这一页,或者直接读它的 HTML,你会看到月供 4,686、还款总额 1,687,076、利息总额 687,076、等额本金首月 6,041、每月递减 8.9。这些是写在标签里的字面文本。脚本一跑,同样的位置变成 4,688 / 1,687,710 / 687,710 / 5,986 / 8.91。那份预渲染的快照总额差了 634 元——把一份手写的旧答案留在源码里就是这个价钱。相信渲染出来的页面;如果你真要两边对照,记住是哪一份把另一份覆盖了。
年限被夹住,利率没人管
在年限栏填 50,它会被弹回 30——脚本把值强制塞进 Math.max(1, Math.min(30, v)) 并同步滑块。但年限栏还有个更安静的特性:它是 parseInt(...) || 1 读的,所以填 0、填非数字都会悄悄变成 1 年(100 万 / 1 年 / 3.85% 实测月供 85,081),只有负数才会触发「年限小于 1」那条拒绝。
利率栏只有一个 min="0.1" 属性,而这类属性只管浏览器自己的校验样式,脚本完全不检查利率。于是默认这页可以毫无意见地给你算一笔 50% 的房贷:每月 41,667,共还 15,000,006,其中 93.3% 是利息。填 0.05% 则给每月 2,799、共还 1,007,540(利息 7,540,占 0.7%)——这是更有用的一个数:三十年期的这个利率,除了本金几乎不多花什么钱。
「按年」那个复选框是实时 onchange,按年与按月两张表是同一套计算的不同聚合,切换不会改用另一套逻辑。英文版同一个复选框写作 Yearly。
利息:三个标签,三种「期」的口径
三个标签不是同一个公式的三种写法,页面自己的文案也这么说。值得钉死的是计息期的口径,因为三个标签在这里是悄悄分家的。
| 标签 | 期的规则 | 每期利率 | 期限怎么读 |
|---|---|---|---|
| 单利 | 连续计息 | 年利率 ÷ 100 | 年 + 月 ÷ 12 |
| 复利 | 每年 n 次,n 是 1 或 12 | 年利率 ÷ 100 ÷ n | 年 + 月 ÷ 12 |
| 定投 | 固定按月 | 年利率 ÷ 100 ÷ 12 | 四舍五入到整月 |
复利标签只有「按年复利」「按月复利」两个按钮。本金 10,000、年利率 5%、10 年,两者给 16,288.95 与 16,470.09——按月那一档高 181.14,这是复利频率本身的差,不是舍入问题。没有按季、按日可用来对比。
期限由「年」「月」两格合成(年 + 月 ÷ 12),定投标签再用 Math.round(期限 × 12) 换回整期。整月填写时这个来回是精确的:1 年 7 个月就是 19 期、6 个月就是 6 期;默认每期 1,000 元时,7 个月的计划给 7 行明细、投入 7,000。取整只在月份栏填小数时才起作用,而那栏虽然写着 step="1",你手打小数它是接受的——1 年 1.5 个月纸面上是 13.5 期,屏幕上显示 14 期(1 年 2.5 个月同样变 15 期),因为 JavaScript 遇半数向上进位。往下取整也一样看不见:1 年 0.4 个月就是一个整 12 期。无论哪种,多出来的那一期都是一整笔投入加一整月利息,所以标题上的总额覆盖的时间比你填的长。
不按按钮就不算
这是最容易咬人的习惯差异。房贷页每个字段都挂着 oninput;利息页一个都没有:输入框是死的,只有按下「计算单利」「计算复利」「计算定投收益」才有结果——以及在你切换标签时,因为切过去会顺手算一次落到的那个标签。
「复利频率」两个按钮点了会立刻重算,因为它们自带一次计算调用;而「扣款时点」下拉框(期末扣款 / 期初扣款)没有绑定任何变更处理,你换成另一个选项,屏幕上还是上一个答案,直到你按按钮为止。
参数一错,数字消失、曲线还在
三个标签都要求本金(或每期投入)、利率、期限三项都大于 0。任何一项不合格,结果区就失去显示类——实测 display: none——总额、收益、收益率一起消失,而且不给任何提示文字。但这道闸门是在画曲线之前 return 的,所以画布保留着你上一次合法计算的图形。在单利标签上先算成功一次、再把本金清空试一下:数字区空了,上面那条增长线一像素都没动。
复利那条曲线会画到你期限之外的一年
单利标签的曲线点是夹着的:默认 10,000 元、5%、期限填 6 个月时,第二个点按 min(1, 0.5) 年计算,读作 10,250——是你真正的六个月余额,横轴标签却写着「1年」。
复利标签不夹。它的曲线点按 i × n 期生成,i 走到 ceil(期限)。同样默认值把期限改成 0 年 6 个月:本息合计 10,246.95,曲线最后一个点却是 10,500——整整多画了一年。(按「按月复利」同一期限是 10,252.62 对末点 10,511.62。)那个点的横轴标签老老实实写着「1年」:曲线画的确实就是第一年。坑在于它和标题数字不一致,而只要你的期限不是整年,这个不一致就存在。
英文版的坐标轴刻度差了十倍,这一页是对的
曲线纵轴刻度由这一行决定:
var labelText = val >= 10000 ? (val / 10000).toFixed(1) + '万' : fmtInt(val);
这里除以一万、接「万」,是对的:5,000,000 显示成 500.0万。同一行代码在英文版里除数没变、单位换成了 K,于是英文版标着 100.0K 的那条刻度其实是 1,000,000。实测英文版按月复利、本金 1,000,000、7%、20 年(总额 4,038,738.85),纵轴打的是 0 / 100.0K / 200.0K / 300.0K / 400.0K / 500.0K。看英文版时把刻度读成「万」,或者忽略刻度文字、只看结果区那几个数字。
定投明细:一期一行,而且导出是真的能用
定投是三个标签里唯一出表格的那个。每一期写一行,再加一行「合计」,所以十年计划是 121 行、三十年是 361 行。默认(每期 1,000 元、8%、10 年)两组数字是:
| 扣款时点 | 期末总资产 | 总收益 | 累计投入 |
|---|---|---|---|
| 期末扣款 | 182,946.04 | 62,946.04 | 120,000 |
| 期初扣款 | 184,165.68 | 64,165.68 | 120,000 |
期初比期末十年多 1,219.64,因为每一笔都多滚了一个月。表里「投入 / 累计投入」两列取整、「资产价值 / 收益」两列保留两位小数,所以合计行是两种精度混在一起的。总期数那一格显示 120期。
另一个极端也值得知道:0 年 1 个月这种只有一期的计划,标题数字和两行表格都会有,但图画不出来——曲线至少需要两个点,于是画布上只剩一句「数据不足,无法绘制图表」。
「导出 CSV」导出的是每一期,不是曲线抽稀后的那约 120 个点。文件头带一个字节序标记,Excel 打开不会乱码;列名是 期数,投入(元),累计投入(元),资产价值(元),收益(元),收益率(%);文件名是 定投明细_ 加当天的 ISO 日期。如果这次页面加载里还没算过定投,按钮会弹窗「没有可导出的数据,请先计算定投收益。」。明细存在一个变量里而不是从表格里回读,所以只要你算过一次定投,切去别的标签后导出仍然可用。
拖动窗口会在停顿 300 毫秒后重算当前可见的标签,画布就是这么跟着新宽度重绘的。
百分比:四个问题,每个都把算式打在答案旁边
四个模式是「A 的 B% 是多少」「A 占 B 的百分之几」「增减百分比」「折扣计算」,字段名分别是 数值/百分比、部分/整体、原值/新值、原价(元)/折扣(% off)。每个模式都把代入过程打出来:200 的 15% = 30、30 ÷ 200 × 100% = 15%、(120 - 100) ÷ 100 × 100% = +20%、200 × 30% = 60 元折扣,实付 200 - 60 = 140 元——这是整页最有用的设计。四个示例按钮(15% 小费 / 8 折优惠 / 涨价 10% / 及格率 60%)会切模式、填两格、再算一次;其中「8 折优惠」填的是 300 与 20,也就是「减两成」。
每一栏都挂着 oninput,改哪格算哪格。但只有第一个模式在页面加载时被计算;另外三个面板显示的是写死在 HTML 里的数字——它们恰好与默认输入吻合,所以直到你在一个看不见的面板上改了默认值再切过去之前,什么都看不出来。
除以零回答「0%」
两个带分母的模式都防了除零,而防的结果是返回 0:
30 ÷ 0给答案0%,算式那行写30 ÷ 0 × 100% = 0%。- 从 0 涨到 120 给
+0%,算式写(120 - 0) ÷ 0 × 100% = +0%。
没有提示、没有 undefined、控件也不会被禁用。算式行把那个零分母明晃晃地摆在你看得到的位置,旁边却是一个自称算出来了的答案。从 0 起步的增长不是 0%,30 也不是「0 的 0%」。这两个答案在你说可信之前,都该先看分母。
保留两位、再剥掉尾零,于是小结果会被压成 0
结果一律 toFixed(2) 再过一遍去尾零,所以 15.00 显示成 15、25.50 显示成 25.5。副作用是:任何小于半百分之一的数会先归零、再连小数点一起丢掉。
| 输入 | 真值 | 显示 |
|---|---|---|
| 200 的 12.5% | 25 | 25 |
| 1 的 33.333% | 0.33333 | 0.33 |
| 0.1 的 0.2% | 0.0002 | 0 |
最后一行在钱的问题上最要命——一笔小额余额上的 0.2% 费率,读起来正好是「零」。
什么都不夹,折扣超过 100% 也算
折扣模式是 原价 × 折扣 ÷ 100 与 原价 − 折扣额,没有任何边界。喂它 150%,得到 省 300 元,实付 -100 元。喂它 −20%,得到 省 -40 元,实付 240 元,算式那行还把这个双重否定原样打出来:200 - -40 = 240。负数输入同样受欢迎:百分比填负,结果就是负的,算式行照抄这个负号。
增减模式自己贴符号(r >= 0 ? '+' : ''),所以 0 显示成 +0%,120 → 100 显示成 -16.67%,算式是 (100 - 120) ÷ 120 × 100%。顺便暴露了那个不对称:同样是 20 这个量的摆动,涨的时候 +20%、跌的时候 −16.67%,因为分母换了——页面会很乐意把两个都给你看,但一个都不解释。
超大输入不是给人看的格式。1e21 取 50% 会打出 499999999999999934464——科学计数法被展开成 21 位字符串,而第十七位往后是浮点的残渣,不是算术。
三台机器怎么配合
真要做一个贷款或投资决定,一个合理的顺序是:
- 凡是「固定还款、余额递减」的都交给房贷页,并且先看利息占比而不是月供。要比的首先是两种还款方式,其次才是不同机构。
- 钱在没有固定还款计划地长大,交给利息页:一次性投入用「复利」,分期投入用「定投」。期限里带月份的,认标题那个数,别认曲线末端。
- 周边的算术核对交给百分比页:一笔手续费是多少现金、一个折扣到底省了多少、报价说涨说跌是相对哪个基数。这几条每一步都要把分母记在心里。
- 在它们交界的地方手工对账:房贷的利息总额和复利的终值不是同一种数,百分比的变化更不是。如果两页给了你一模一样的数字,那更值得复核而不是庆祝。
这几页的边界
- 没有哪一页从任何地方取利率;三台机器模型的都是整个期限内永不变化的利率。没有提前还款、没有利率重置、没有逐年加档、没有把未付利息资本化。
- 没有波动、没有违约。定投明细和复利曲线都是确定性路径,不是某个分布下的期望值。
- 房贷页不含手续费、税费、保险、评估费,所以它给的是这笔贷款的成本,不是这笔交易的成本。
- 任何输入都不落存储。刷新任何一页,默认值就回来了;不存本地、不外发——三页全在自己脚本里算完,它们唯一会产出的文件是你主动点出来的那份定投 CSV。
- 上面提到的所有舍入都只影响显示。要一分一分地对账,请把页面上打出来的公式搬进表格软件算,别去加那些表格行。