个税模拟器计算演示版,纯js源码分享——看到这个标题的时候,我第一反应是:终于有人愿意把这件“看着简单、做起来全是细节”的事情单独拆出来了。个税计算器本质上不是代码量的问题,而是税法规则怎么翻译成程序逻辑的问题。如果只是做“工资减5000再乘税率”,结果会和工资条差得离谱;但如果把累计预扣法、专项附加扣除、全年综合所得这些概念全塞进去,又容易把Demo做成一个连自己都不想维护的庞然大物。
这个“计算演示版”的定位其实很聪明:不追求覆盖所有真实场景,而是用纯JS、单页面、零依赖的方式,把计税逻辑跑通,让读者看得懂每一步计算,也能直接复制源码改着玩。这篇文章我打算从计税规则拆起,再讲核心函数怎么写,最后把我在实际调试中踩过的坑一并整理出来,适合前端初学者拿来练手,也适合HR、财务或普通打工人快速估算自己的税后收入。
1. 个税模拟器的核心价值:先搞懂计税规则,再谈写代码
1.1 一个Demo背后藏着的完整税制逻辑
很多人以为个税计算就是简单的分段函数:月薪减去5000,落在哪个区间就按哪个税率算。这个理解在老个税时代勉强成立,但在累计预扣法之下已经不太准确了。
新个税的核心是“综合所得”。工资薪金、劳务报酬、稿酬、特许权使用费合并成综合所得,按纳税年度合并计算。日常发工资时公司不会等到年底才扣税,而是每个月先“预扣预缴”,到次年再汇算清缴、多退少补。预扣预缴用的就是累计预扣法,思路是把截至本月的所有收入和扣除项都累加起来,按年度税率表算出“累计应预扣税额”,再减去之前已经预扣过的税额,剩下就是本月要交的。
举个例子会更直观:月薪15000元、没有社保也没有专项附加扣除的情况下,每月应纳税所得额是10000元。如果按“月度估算”的做法,10000元落在10%税率档,月税大概(10000×12×10%-2520)/12=790元。但累计预扣法下,前面几个月的累计应纳税所得额可能还在3%档,每月只预扣300元上下,等到后期才逐渐提到10%档。全年总税额虽然一致,每个月的数字却是前低后高。这种走势不亲自动手跑一遍数据,很难体会到。
所以这个演示版的价值不只是算出一个数,而是把“月度估算”和“累计预扣”这两种口径都做出来,让人直观看到差异。源码里我会同时提供两个计算函数,你可以切换着看结果。
1.2 哪些人会用到这类计算器
从我在实际工作里接触到的需求来看,个税模拟器的受众远比想象中杂。前端开发者拿它当练手项目,能同时锻炼表单交互、数据处理和边界判断;做薪酬的HR朋友需要一个不依赖第三方接口的内部测算工具,输入工资就能快速换算出税后成本;准备跳槽的普通打工人更是需要它来模拟“涨薪2000到底到手能多多少”——这个问题的答案往往和直觉有差距,因为税率是跳档的。
演示版的意义在于“轻”。不需要登录、不需要数据库、不需要后端服务,一个HTML文件就能在浏览器里跑起来。如果你只是想快速验证一段计税逻辑,纯JS方案是最省事的;如果你想把它嵌到内部管理系统里,把计算函数抽出去,换个UI外壳就能用。后面我会专门讲函数怎么抽。
2. 算准个税的五个关键输入:收入、减除、社保、专项附加、税率表
2.1 基本减除费用与社保公积金扣除
个税计算的起点是“应纳税所得额”,不是工资全额。先要减去每月5000元的基本减除费用,也就是常说的“起征点”。这5000元不分城市、不分行业、不看学历,每个人每个月都有。
接着是“专项扣除”,主要指个人缴纳的养老保险、医疗保险、失业保险和住房公积金,通常统称“三险一金”。这部分因为是税前扣除,所以会直接影响计税基数。注意不同城市的社保比例和公积金缴存比例差别很大,公积金的比例可以从5%到12%,哪怕税前工资一样,税后到手也可能差几百块。演示版里我把社保和公积金合并成一个输入框,按“每月个人缴纳部分”整体填写,这样既简化了界面,又不会丢失计算精度。
还有“税延养老保险”等特定项目,普通模拟器一般不考虑,感兴趣的话可以在源码里预留一个“其他扣除”字段,逻辑上加一行代码就行。
2.2 专项附加扣除的录入方式
专项附加扣除是很多人搞不清楚的部分。子女教育、继续教育、大病医疗、住房贷款利息、住房租金、赡养老人、3岁以下婴幼儿照护,一共七类,每类有各自的扣除标准和适用条件。演示版如果把这些做成七个输入框,界面会显得很臃肿,而且政策细节随时可能调整,维护成本很高。
所以更聪明的做法是合并成一个“每月专项附加扣除总额”。用户按自己的实际情况估算一个总额填进去,计算函数只关心总数是多少,并不关心它是哪几项组成的。这样代码保持简洁,同时不牺牲通用性。
我在实际使用中习惯给自己设一个参考值:有房贷的填1000,有小孩上学的再填2000,赡养老人如果是独生子女再填2000,大致加总就行。如果你要做一个团队内部用的版本,可以考虑把这些扣除项分开,但我始终建议默认视图保持“总额模式”,需要明细再展开一个高级面板。具体政策标准会随政策变化,演示版里不写死,以最新口径为准即可。
2.3 税率表与速算扣除数的数学原理
综合所得适用的年度税率表是七级超额累进税率,从3%到45%。表格长这样:
| 年度应纳税所得额(元) | 预扣率 | 速算扣除数(元) |
|---|---|---|
| 不超过36000元 | 3% | 0 |
| 超过36000元至144000元 | 10% | 2520 |
| 超过144000元至300000元 | 20% | 16920 |
| 超过300000元至420000元 | 25% | 31920 |
| 超过420000元至660000元 | 30% | 52920 |
| 超过660000元至960000元 | 35% | 85920 |
| 超过960000元 | 45% | 181920 |
速算扣除数看起来像个魔法数字,实际上只是为了避免分段计算的繁琐。以第二档为例,分段算税额是“36000×3% + (应纳税所得额-36000)×10%”,化简之后就是“应纳税所得额×10%-2520”,这个2520就是速算扣除数。你用分段累加和用全额乘税率再减速算扣除数,结果完全一致,但后者在代码里只需要一次乘法一次减法,效率和可读性都好得多。
代码里最关键是这张表的数据结构。我建议用数组加对象来存,每一项包含上限、税率和速算扣除数,这样后续增删档位都不需要改计算逻辑。千万不要把档位写死在if-else里,否则每次政策调整都要翻代码。
3. 纯JS实现:核心函数、页面组装与源码细节
3.1 页面骨架与交互设计
这个演示版的目标是“开箱即用”,所以一个HTML文件就够。页面结构分上下两块:上半部分是输入区,下半部分是结果区。
输入区至少需要四个字段:每月税前工资、每月社保公积金个人缴纳部分、每月专项附加扣除总额、计算模式选择(月度估算或累计预扣)。结果区需要展示:月度应纳税所得额、年度应纳税所得额、适用税率、速算扣除数、本月应缴个税、到手收入。
交互上我强烈推荐用input事件实时刷新,不需要点击“计算”按钮。用户一改数字,结果就跟着变,这种即时反馈特别适合模拟器,能让人反复拖动数字观察税率的跳档变化。用change事件在失焦后才更新,体验会差一大截。
3.2 月度估算模式的计算函数
月度估算模式适合快速出数,逻辑是“按当前月收入推算全年水平再均摊回每月”。核心代码是这样:
// 年度综合所得税率表 const TAX_BRACKETS = [ { upper: 36000, rate: 0.03, deduction: 0 }, { upper: 144000, rate: 0.10, deduction: 2520 }, { upper: 300000, rate: 0.20, deduction: 16920 }, { upper: 420000, rate: 0.25, deduction: 31920 }, { upper: 660000, rate: 0.30, deduction: 52920 }, { upper: 960000, rate: 0.35, deduction: 85920 }, { upper: Infinity, rate: 0.45, deduction: 181920 } ]; function getBracket(annualTaxable) { return TAX_BRACKETS.find(item => annualTaxable <= item.upper); } function estimateMonthlyTax(monthlyIncome, socialSecurity, extraDeduction) { const monthlyTaxable = monthlyIncome - 5000 - socialSecurity - extraDeduction; if (monthlyTaxable <= 0) { return { monthlyTaxable: 0, annualTaxable: 0, rate: '0%', quickDeduction: 0, tax: 0, takeHome: monthlyIncome - socialSecurity }; } const annualTaxable = monthlyTaxable * 12; const bracket = getBracket(annualTaxable); const annualTax = annualTaxable * bracket.rate - bracket.deduction; const monthlyTax = Math.round(annualTax / 12); return { monthlyTaxable, annualTaxable, rate: (bracket.rate * 100).toFixed(0) + '%', quickDeduction: bracket.deduction, tax: monthlyTax, takeHome: monthlyIncome - socialSecurity - monthlyTax }; }注意这里的关键是“把月度应纳税所得额乘以12换成年化”。比如月薪15000、社保1000、专项附加1000,月度应纳税所得额就是8000元,年化96000元,落在第二档(10%),年税就是96000×10%-2520=7080元,月均590元。这个数字适合用来估“全年平均税负”,但不等于每个月工资条上的数字。
3.3 累计预扣模式的实现
如果你想让模拟器更贴近真实工资条,就必须实现累计预扣法。它的输入不再是“本月收入”,而是“截至本月的累计收入”“累计社保”“累计专项附加”和“累计已预扣税额”。
function calcCumulativeTax({ cumulativeIncome, cumulativeSocialSecurity, cumulativeExtraDeduction, monthCount, paidTax }) { const monthlyThreshold = 5000; const cumulativeTaxable = cumulativeIncome - monthlyThreshold * monthCount - cumulativeSocialSecurity - cumulativeExtraDeduction; if (cumulativeTaxable <= 0) { return { currentTax: 0, cumulativeTax: 0, cumulativeTaxable: 0 }; } const bracket = getBracket(cumulativeTaxable); const cumulativeTax = Math.max( Math.round(cumulativeTaxable * bracket.rate - bracket.deduction), 0 ); const currentTax = Math.max(cumulativeTax - paidTax, 0); return { currentTax, cumulativeTax, cumulativeTaxable, rate: (bracket.rate * 100).toFixed(0) + '%', quickDeduction: bracket.deduction }; }这个函数里最容易被忽略的是累计减除费用:不是简单乘以当前月份数,而是“5000×月数”。1月用一次5000,12月用60000,这样全年累计减除费用正好达到6万元。
用这个函数逐月跑一遍,你会看到每个月的税额变化。我录了一组数据供参考:月薪15000、社保1000、专项附加1000,1月应纳税所得额8000,累计应纳税8000×3%=240元;到5月累计应纳税所得额40000,累计应纳税40000×10%-2520=1480元,减去前4个月已预扣的960元,5月应缴520元。这种起伏就是累计预扣法的典型表现。
演示版可以在输入区加一个“已预扣税额”字段,每次算完当前月,把结果填回去,就能连续滚动计算下去。
3.4 组装:一个可运行的迷你Demo
把页面串起来并不复杂。HTML部分用最朴素的原生表单,结果区域用四个只读字段展示。核心绑定就两段:
<input type="number" id="monthlyIncome" value="15000" /> <input type="number" id="socialSecurity" value="1000" /> <input type="number" id="extraDeduction" value="1000" /> <select id="calcMode"> <option value="estimate">月度估算</option> <option value="cumulative">累计预扣</option> </select>const inputs = ['monthlyIncome', 'socialSecurity', 'extraDeduction']; inputs.forEach(id => { document.getElementById(id).addEventListener('input', updateResult); }); document.getElementById('calcMode').addEventListener('change', updateResult); function updateResult() { const income = parseFloat(document.getElementById('monthlyIncome').value) || 0; const social = parseFloat(document.getElementById('socialSecurity').value) || 0; const extra = parseFloat(document.getElementById('extraDeduction').value) || 0; const mode = document.getElementById('calcMode').value; let result; if (mode === 'estimate') { result = estimateMonthlyTax(income, social, extra); } else { // 累计预扣模式下,演示版默认按第1个月计算,便于理解 result = calcCumulativeTax({ cumulativeIncome: income, cumulativeSocialSecurity: social, cumulativeExtraDeduction: extra, monthCount: 1, paidTax: 0 }); } // 把result里的字段渲染到页面上 }这里的细节是parseFloat加||0,因为空字符串转成数字会变0,但NaN一旦出现,会污染所有下游结果。初始值给一个合理的demo数据,比如收入15000、社保1000、专项1000,用户打开就能看到一组有意义的数字,比空表单更容易上手。
样式方面不用过度设计,用flex布局居中,结果区数字加大加粗,税率档位变化时用CSS类切换颜色,就能有不错的观感。一个真正吸引人的演示版,交互反馈比视觉特效重要得多。
4. 常见问题与排查实录
4.1 为什么模拟器和工资条对不上
这是被问得最多的问题。原因通常有三个:一是公司使用的社保公积金基数和你的估算不同;二是专项附加扣除申报时间有延迟,比如年中才补录,会导致前面月份补扣;三是计税方式差异,少数公司内部系统是按简化月度表估算扣缴,而不是严格用累计预扣法。
累计预扣法还有个容易忽略的细节:年度中间换工作,新公司不知道你之前的累计收入和已预扣税额,会重新从第一个月开始累计。这会导致全年实际预扣税额低于应缴额,次年汇算时需要补税。这也是为什么很多人第一次做汇算清缴会莫名其妙补一笔钱。
你可以在模拟器里加一个“上家单位已预扣税额”输入项,把跳槽场景也模拟进去,这样不光能算月税,还能估算换工作的税务影响。这个扩展特别适合对薪酬敏感的技术人。
4.2 年终奖单独计税怎么模拟
年终奖是另一个常见坑。优惠政策有效期内,年终奖可以选择单独计税,也可并入当年综合所得计税,谁能少交就选谁。单独计税的方法是:把年终奖除以12得到“月均奖金”,查月度税率表确定级次,再用年终奖全额乘以税率减去速算扣除数。
月度税率表和年度表不是同一个,单独计税用的是另一张:
| 月均应纳税所得额 | 税率 | 速算扣除数 |
|---|---|---|
| 不超过3000元 | 3% | 0 |
| 超过3000元至12000元 | 10% | 210 |
| 超过12000元至25000元 | 20% | 1410 |
| 超过25000元至35000元 | 25% | 2660 |
| 超过35000元至55000元 | 30% | 4410 |
| 超过55000元至80000元 | 35% | 7160 |
| 超过80000元 | 45% | 15160 |
对应的函数很简单:
const MONTHLY_BRACKETS = [ { upper: 3000, rate: 0.03, deduction: 0 }, { upper: 12000, rate: 0.10, deduction: 210 }, { upper: 25000, rate: 0.20, deduction: 1410 }, { upper: 35000, rate: 0.25, deduction: 2660 }, { upper: 55000, rate: 0.30, deduction: 4410 }, { upper: 80000, rate: 0.35, deduction: 7160 }, { upper: Infinity, rate: 0.45, deduction: 15160 } ]; function calcBonusTax(bonus) { const monthlyEquivalent = bonus / 12; const bracket = MONTHLY_BRACKETS.find(item => monthlyEquivalent <= item.upper); return Math.round(bonus * bracket.rate - bracket.deduction); }写代码只要20行,但真正的坑在于“对比”。年终奖3.6万元和3.7万元之间可能相差好几百的税,因为跨档了。我建议演示版在显示单独计税结果的同时,把“并入综合所得”的差额也算出来,这样用户才能做出选择。如果只给一个数字,很容易误导人。
4.3 输入异常与边界处理
计算器类工具最常见的翻车点不是算法,而是用户输入了奇怪的值。我从自己的调试记录里整理了这几个高发问题:
第一个是空值。很多浏览器把清空的数字输入框value当作空字符串,parseFloat后得到NaN,一旦NaN进入计算,整个结果区域都变成NaN。解决方法是parseFloat后立即用||0兜底,或者判断isNaN后赋0。
第二个是负数和超界。用户可能手滑输入负数社保,或者社保金额超过工资。我的处理是计算前做一次clamp,把负数归零;如果社保公积金大于工资,提示用户检查输入,而不是默默算出一个月薪为负的荒谬结果。
第三个是浮点精度。虽然税表里都是整数金额,但除法会产出0.1之类的不精确值。所有金额展示前务必Math.round一次,保留到元即可,没必要显示小数点后两位。
第四个是极大值。输入1亿工资时,年化应纳税所得额会非常大,但税率表最后一项的upper是Infinity,所以查找不会出错。可速算扣除数在旁边,浮点运算也还稳定,只是UI上数字可能过长。演示版可以加一个toLocaleString用于展示,提升可读性。
5. 从演示版到生产级工具的扩展思路
5.1 把税率配置变成数据
我在前文已经建议用数组存储税率表,如果只在一个HTML文件里用,写死没问题。可一旦要长期维护,最好把税率表抽成独立JSON或配置项。这样政策档位调整时,只改数据不动逻辑,比翻代码找if-else高效太多。
更进一步,可以把社保比例、公积金比例、专项附加上限全部做成配置,不同公司、不同城市的人员打开同一个工具,看到的是各自适用的参数。配置化是个税计算器从“玩具”走向“内部工具”的第一步。
5.2 社保基数按城市差异化
社保这块水很深。每个城市都有缴费基数上下限,像北京、上海、深圳的社保基数范围差距很大,公积金比例也不一样。演示版里用一个总额输入框规避了这个问题,但生产工具里至少要支持“自动带出城市参数”。
实际做法可以内置一张城市参数表:默认社保比例、公积金比例、缴费基数上下限。用户选择城市后自动填充,还想手动微调就开放编辑。这个功能不难,但信息收集和更新是个持续工作,所以演示版不推荐内置太多城市,留两三个样例城市就够。
5.3 框架迁移与组件化
如果你的项目已经用Vue或React,完全可以把计算函数抽成独立的纯函数模块,不依赖任何DOM操作。原有的estimateMonthlyTax和calcCumulativeTax可以直接复用,只是把输入输出变成props和state。
我建议函数签名保持纯函数风格:入参是一个对象,出参也是一个对象,内部不碰DOM。这样不仅方便测试,也方便未来做单元测试——税务计算这种逻辑,给一组输入断言一组输出,比什么都可靠。
迁移到组件时,注意把“展示”和“计算”彻底分离。页面只负责采集输入和渲染结果,所有税款计算都交给纯函数。哪怕税表政策变了,也只改配置文件,页面一行不用动。
我在实际使用中发现,很多内部工具卡在“能用”和“好用”之间,差的往往不是功能,而是数据边界处理。个税模拟器这类小工具,与其堆功能,不如先把计算口径做扎实,把异常输入挡住,把每个数字的来龙去脉交代清楚。这样无论别人是拿来估算工资还是研究计税逻辑,都能快速建立起信任感。
最后说一个我常用的验证技巧:在源码里加一个“测试用例”按钮,自动跑三四组典型数据,比如“月薪刚好5000”“月薪15000跨档”“年终奖36000临界点”,用断言比对预期结果。每次改完代码点一下,全部通过再发布。这个习惯帮我挡住了好几次因为改税表而引入的回归问题。你可以把这个思路也带到其他财务类小工具的开发里去。