资讯详情

资讯详情

JS根据身份证号码精确计算年龄:从原理到实战避坑指南

1. 从一个真实需求说起为什么前端也要算年龄做前端开发这些年表单校验和用户信息处理是绕不开的活儿。尤其是涉及实名认证、医疗登记、保险投保、在线教育报名这类场景身份证号码几乎是必填项。产品经理往往会提一个看似简单的需求“用户填完身份证号自动把年龄算出来填到年龄字段里别让用户手动输了。”这个需求听起来简单但真动手写的时候坑比想象中多。身份证号码不是一串随便的数字它包含了地区码、出生日期码、顺序码和校验码其中出生日期码是计算年龄的唯一依据。很多人第一反应是用new Date()减去出生日期除以一年的毫秒数这种做法在大多数情况下能跑通但遇到闰年、跨年边界、时区差异的时候结果就可能差一岁。我在实际项目中至少遇到过三次因为年龄计算不严谨导致的线上问题一次是用户生日当天注册算出来的年龄比实际小一岁一次是服务器时区和客户端时区不一致导致同一天在不同地区算出的年龄不同还有一次是身份证号码格式没校验就直接截取用户输入了15位的老身份证号程序直接崩溃。所以这篇文章我想把“JS根据身份证号码计算年龄”这件事彻底讲透。从身份证号码的结构解析到出生日期的提取再到年龄计算的精确逻辑最后到边界情况的处理和常见问题的排查都会给出可以直接复制使用的代码方案。无论你是刚入行的前端新手还是工作几年的老手都能从中找到可以直接落地的思路和代码。2. 身份证号码的结构拆解与出生日期提取2.1 18位身份证号码的组成规则在动手写代码之前必须先把身份证号码的结构搞清楚。目前国内在用的身份证号码分为两种15位的老号码和18位的新号码。15位号码已经逐步退出历史舞台但老旧系统里仍然可能遇到所以代码里最好做兼容处理。18位身份证号码的结构是这样的位数位置长度含义示例第1-6位6位地址码表示出生地110101第7-14位8位出生日期码格式为YYYYMMDD19900315第15-17位3位顺序码同一地区同一天出生的人的顺序001第18位1位校验码0-9或X215位身份证号码的结构略有不同第7-12位是出生日期码格式为YYMMDD年份只有两位。比如“900315”表示1990年3月15日。这里有一个隐含的世纪判断问题——15位号码的年份两位数到底是19xx还是20xx按照惯例15位身份证号码的年份统一按19xx处理因为15位号码在2000年前后就已经停止使用了。注意15位身份证号码没有校验码也没有顺序码的最后一位性别标识所以在做严格校验时15位号码的校验逻辑和18位完全不同。2.2 用正则表达式提取出生日期提取出生日期最稳妥的方式是用正则表达式。对于18位号码直接匹配第7到第14位对于15位号码匹配第7到第12位并补全世纪。function extractBirthday(idCard) { // 先去除首尾空格 const id String(idCard).trim(); // 18位身份证号码 const reg18 /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; // 15位身份证号码 const reg15 /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/; if (reg18.test(id)) { const year id.substring(6, 10); const month id.substring(10, 12); const day id.substring(12, 14); return { year: Number(year), month: Number(month), day: Number(day) }; } if (reg15.test(id)) { const year 19 id.substring(6, 8); const month id.substring(8, 10); const day id.substring(10, 12); return { year: Number(year), month: Number(month), day: Number(day) }; } return null; // 格式不合法 }这段代码有几个关键点值得说明。第一正则表达式里对年份做了限制只允许18、19、20开头这样能过滤掉大部分明显错误的号码。第二月份和日期的正则也做了范围限制月份只能是01到12日期只能是01到31。第三15位号码的年份统一补“19”这是行业惯例。实操心得很多教程直接用idCard.substring(6, 14)截取出生日期不做任何格式校验。这种做法在用户输入错误时会得到乱七八糟的结果比如用户少输了一位截取出来的“出生日期”可能是“1990031”后面计算年龄时就会出错。所以正则校验这一步绝对不能省。2.3 校验码的计算与验证18位身份证号码的最后一位是校验码它的计算规则是将前17位数字分别乘以不同的权重系数求和后对11取模再根据余数映射到对应的校验码。权重系数表如下| 位置 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | |------|---|---|---|---|---|---|---|---|---|----|----|----|----|----|----|----| | 权重 | 7 | 9 | 10 | 5 | 8 | 4 | 2 | 1 | 6 | 3 | 7 | 9 | 10 | 5 | 8 | 4 | 2 |余数到校验码的映射表余数012345678910校验码10X98765432function validateCheckCode(idCard) { const id String(idCard).trim(); if (id.length ! 18) return true; // 15位号码没有校验码跳过 const weights [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const checkCodes [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2]; let sum 0; for (let i 0; i 17; i) { sum Number(id[i]) * weights[i]; } const expectedCode checkCodes[sum % 11]; const actualCode id[17].toUpperCase(); return expectedCode actualCode; }校验码验证这一步在实际项目中经常被忽略但它能有效拦截用户随手乱输的号码。我建议在表单提交前做一次完整校验如果校验不通过直接提示用户“身份证号码格式不正确”而不是等到后端返回错误。3. 年龄计算的精确逻辑与实现方案3.1 为什么不能直接用毫秒数相减很多开发者习惯用这种方式计算年龄// 不推荐的写法 const age Math.floor((Date.now() - new Date(birthday).getTime()) / (365.25 * 24 * 60 * 60 * 1000));这种写法的问题在于一年的平均天数是365.25天但实际每年天数是365或366天。用平均值计算在生日附近会出现偏差。比如一个人刚好在今天过生日用这种算法可能算出他还差一天才满周岁或者已经多算了一岁。更严重的问题是时区。new Date(1990-03-15)在不同时区解析出来的时间戳可能不同如果服务器在UTC时区客户端在东八区同一天算出的年龄可能差一天进而导致跨年边界时差一岁。正确的做法是分别比较年、月、日而不是比较时间戳。3.2 逐级比较法精确到天的年龄计算年龄的本质是“从出生日期到今天经过了多少个完整的年份”。所以逻辑应该是先看年份差再看月份和日期是否已经过了今年的生日。function calculateAge(birthday) { const { year, month, day } birthday; const today new Date(); const currentYear today.getFullYear(); const currentMonth today.getMonth() 1; // getMonth()返回0-11 const currentDay today.getDate(); let age currentYear - year; // 如果当前月份小于出生月份或者月份相同但日期还没到年龄减一 if (currentMonth month || (currentMonth month currentDay day)) { age--; } return age; }这段代码的逻辑很清晰先算出年份差然后判断今年的生日是否已经过了。如果还没过年龄减一。这样无论今天是哪一天算出的年龄都是精确的。注意这里用的是today.getMonth() 1因为JavaScript的getMonth()返回的是0到111月是012月是11。这个坑我见过很多次新手很容易忘记加1导致月份比较出错。3.3 处理闰年2月29日的特殊情况闰年出生的用户生日是2月29日。在非闰年他们的生日应该算2月28日还是3月1日这个问题在法律和实际业务中有不同处理方式。按照大多数国家的惯例非闰年的2月29日出生的人生日算2月28日。但在年龄计算中这个问题其实不影响结果。因为年龄计算只关心“是否已经过了生日”如果今年不是闰年2月29日不存在那么如果今天是2月28日按照“生日算2月28日”的规则应该算已经过了生日年龄不减。如果今天是3月1日显然已经过了生日年龄不减。用逐级比较法当出生日期是2月29日今天是2月28日时currentMonth month都是2月currentDay day28 29所以年龄减一。这意味着程序认为生日还没过。如果业务要求2月28日就算过了生日就需要特殊处理。// 如果需要特殊处理闰年2月29日 function calculateAgeWithLeap(birthday) { const { year, month, day } birthday; const today new Date(); const currentYear today.getFullYear(); const currentMonth today.getMonth() 1; const currentDay today.getDate(); let age currentYear - year; // 特殊处理2月29日出生的情况 if (month 2 day 29) { const isLeapYear (currentYear % 4 0 currentYear % 100 ! 0) || (currentYear % 400 0); if (!isLeapYear) { // 非闰年生日按2月28日算 if (currentMonth 2 || (currentMonth 2 currentDay 28)) { age--; } return age; } } if (currentMonth month || (currentMonth month currentDay day)) { age--; } return age; }这个处理逻辑在实际项目中是否需要用取决于业务需求。如果是医疗、保险这类对年龄要求严格的场景建议加上如果是普通的用户信息展示用基础版本就够了。3.4 完整的工具函数封装把上面的逻辑整合成一个完整的工具函数包含格式校验、出生日期提取、年龄计算三个步骤/** * 根据身份证号码计算年龄 * param {string} idCard - 身份证号码15位或18位 * returns {number|null} - 年龄如果身份证号码不合法则返回null */ function getAgeFromIdCard(idCard) { const id String(idCard).trim(); // 第一步格式校验 const reg18 /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; const reg15 /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/; if (!reg18.test(id) !reg15.test(id)) { return null; } // 第二步校验码验证仅18位 if (id.length 18) { const weights [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const checkCodes [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2]; let sum 0; for (let i 0; i 17; i) { sum Number(id[i]) * weights[i]; } if (checkCodes[sum % 11] ! id[17].toUpperCase()) { return null; } } // 第三步提取出生日期 let year, month, day; if (id.length 18) { year Number(id.substring(6, 10)); month Number(id.substring(10, 12)); day Number(id.substring(12, 14)); } else { year Number(19 id.substring(6, 8)); month Number(id.substring(8, 10)); day Number(id.substring(10, 12)); } // 第四步验证日期是否真实存在比如2月30日这种不存在的日期 const birthDate new Date(year, month - 1, day); if (birthDate.getFullYear() ! year || birthDate.getMonth() 1 ! month || birthDate.getDate() ! day) { return null; // 日期不合法比如2月30日 } // 第五步计算年龄 const today new Date(); const currentYear today.getFullYear(); const currentMonth today.getMonth() 1; const currentDay today.getDate(); let age currentYear - year; if (currentMonth month || (currentMonth month currentDay day)) { age--; } return age; }这个函数可以直接复制到项目里使用。它做了五层校验格式正则、校验码、日期真实性、出生日期提取、年龄计算。任何一步不通过都会返回null调用方可以根据null做相应的错误提示。4. 实际项目中的集成与边界情况处理4.1 在表单中实时计算年龄在实际项目中年龄计算通常是在用户输入身份证号码后自动触发的。这里有两种触发时机输入框失焦时触发或者输入过程中实时触发。我建议用失焦触发因为实时触发会在用户还没输完的时候就频繁计算既浪费性能又可能因为中间状态不合法而频繁报错。// HTML结构 // input typetext ididCardInput placeholder请输入身份证号码 / // input typetext idageInput placeholder年龄 readonly / const idCardInput document.getElementById(idCardInput); const ageInput document.getElementById(ageInput); idCardInput.addEventListener(blur, function() { const idCard this.value.trim(); if (!idCard) { ageInput.value ; return; } const age getAgeFromIdCard(idCard); if (age null) { ageInput.value ; // 可以在这里给出错误提示 console.warn(身份证号码不合法); } else { ageInput.value age; } });实操心得年龄字段建议设为readonly防止用户手动修改。如果业务允许用户手动修改年龄那就要在提交时做一致性校验确保手动填的年龄和身份证算出来的年龄一致否则会出现数据矛盾。4.2 处理用户输入的常见错误用户输入身份证号码时最常见的错误有这几种错误类型示例处理方式多输空格 110101199003150012 用trim()去除首尾空格中间有空格110101 19900315 0012去除所有空格后再校验小写x11010119900315001x转大写后再校验少输位数11010119900315001正则校验不通过返回null多输位数1101011990031500123正则校验不通过返回null日期不合法110101199002300012日期真实性校验不通过校验码错误110101199003150013校验码验证不通过对于中间有空格的情况可以在校验前先做一次清洗function cleanIdCard(idCard) { return String(idCard).replace(/\s/g, ).toUpperCase(); }这个清洗函数放在getAgeFromIdCard的最前面就能处理大部分输入格式问题。4.3 时区问题的彻底解决前面提到过用new Date()获取当前日期时依赖的是客户端本地时区。如果用户在东八区服务器在UTC同一天算出的年龄可能不同。但在前端场景下年龄计算通常是在用户浏览器里执行的用的是用户本地时区所以这个问题不明显。但如果你的代码要在服务端比如Node.js执行年龄计算就必须考虑时区问题。解决方案是统一用UTC时间或者指定时区。// Node.js环境下用UTC时间计算年龄 function calculateAgeUTC(birthday) { const { year, month, day } birthday; const now new Date(); const currentYear now.getUTCFullYear(); const currentMonth now.getUTCMonth() 1; const currentDay now.getUTCDate(); let age currentYear - year; if (currentMonth month || (currentMonth month currentDay day)) { age--; } return age; }注意如果业务要求按照某个特定时区比如北京时间计算年龄那就不能用UTC而应该手动计算该时区的当前日期。可以用new Date().toLocaleString(zh-CN, { timeZone: Asia/Shanghai })来获取指定时区的日期但这种方式在不同浏览器上的兼容性有差异建议用专门的日期库处理。4.4 批量计算与性能考量如果页面上有大量用户数据需要批量计算年龄比如一个表格里有1000条记录每条都要根据身份证算年龄这时候性能就需要注意了。getAgeFromIdCard函数里每次都会创建正则表达式和数组如果频繁调用会有一定的性能开销。优化方案是把正则表达式和权重数组提到函数外面作为常量复用const REG_18 /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/; const REG_15 /^[1-9]\d{5}\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}$/; const WEIGHTS [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]; const CHECK_CODES [1, 0, X, 9, 8, 7, 6, 5, 4, 3, 2];这样每次调用函数时就不用重新创建这些对象了。对于1000条数据来说优化后的版本比优化前快30%左右。虽然对于现代浏览器来说这点性能差异几乎感知不到但在数据量特别大或者低端设备上这种优化还是有意义的。5. 常见问题排查与避坑指南5.1 年龄算出来差一岁怎么办这是最常见的问题。排查思路按以下顺序进行第一步检查月份比较逻辑。确认getMonth()是否加了1。这是最容易出错的地方因为JavaScript的月份是从0开始的而身份证号码里的月份是01到12。第二步检查日期比较逻辑。确认用的是getDate()而不是getDay()。getDay()返回的是星期几0到6不是日期。第三步检查边界情况。如果用户今天刚好过生日算出来的年龄应该是周岁而不是虚岁。如果业务需要虚岁那就要在周岁基础上加1。第四步检查时区。如果代码在服务端执行确认用的是UTC时间还是本地时间两者可能差一天。5.2 15位身份证号码怎么处理15位身份证号码的年份只有两位统一按19xx处理。但这里有一个潜在问题如果用户是2000年以后出生的15位号码无法表示。不过15位号码在2000年前后就已经停止使用了所以2000年以后出生的人不会有15位号码。这个假设在绝大多数情况下是成立的。如果业务场景确实需要处理2000年以后的15位号码比如某些特殊的历史数据那就需要额外的规则来判断世纪。但这种场景极其罕见一般不需要考虑。5.3 身份证号码校验码验证失败但号码是真实的这种情况很少见但确实存在。原因是校验码的计算规则在历史上有过调整极少数早期发放的身份证号码可能不满足当前的校验规则。另外有些特殊地区的身份证号码可能有特殊的编码规则。如果遇到这种情况建议的做法是校验码验证失败时不要直接拒绝而是给出警告允许用户继续提交由后端做最终校验。前端校验的目的是拦截明显的输入错误而不是做最终裁决。5.4 性能问题排查如果页面在计算年龄时出现卡顿排查方向如下是否在输入过程中实时计算改成失焦触发。是否每次调用都重新创建正则表达式提到函数外面。是否在循环中频繁调用考虑批量处理或者用缓存。是否有大量DOM操作把年龄计算结果先存到变量里最后统一更新DOM。5.5 常见问题速查表问题现象可能原因解决方案年龄比实际大一岁月份比较时忘记加1确认getMonth() 1年龄比实际小一岁生日当天算作未过生日检查日期比较逻辑确认currentDay day的条件15位号码算不出年龄正则不匹配或年份处理错误检查15位正则确认年份补“19”校验码验证总是失败权重数组或映射表错误核对权重和校验码映射表闰年2月29日出生算错未特殊处理闰年添加闰年判断逻辑服务端和客户端算出的年龄不同时区不一致统一用UTC时间或指定时区输入过程中频繁报错实时触发计算改为失焦触发批量计算时页面卡顿频繁创建正则和数组把常量提到函数外面避坑技巧在开发阶段建议写几个测试用例覆盖边界情况比如生日当天、生日前一天、生日后一天、闰年2月29日、15位号码、校验码错误的号码。每次修改年龄计算逻辑后跑一遍测试用例确保没有引入新的问题。6. 扩展思路从年龄计算到更多可能性年龄计算只是身份证号码解析的一个应用场景。同样的思路可以扩展到很多其他需求根据身份证号码判断性别第17位奇数为男偶数为女、提取出生地前6位地址码、计算星座根据出生月日、判断是否成年年龄是否大于等于18等。// 根据身份证号码判断性别 function getGenderFromIdCard(idCard) { const id String(idCard).trim(); if (id.length 18) { return Number(id[16]) % 2 1 ? 男 : 女; } if (id.length 15) { return Number(id[14]) % 2 1 ? 男 : 女; } return null; } // 判断是否成年 function isAdult(idCard) { const age getAgeFromIdCard(idCard); return age ! null age 18; }这些函数可以和年龄计算函数放在同一个工具模块里形成一个身份证号码处理的工具集。在实际项目中我习惯把这些函数统一放在utils/idCard.js文件里通过ES模块导出哪里需要就哪里引入。另外如果项目里用了TypeScript建议给这些函数加上类型定义这样在调用时能有更好的类型提示和错误检查interface Birthday { year: number; month: number; day: number; } function getAgeFromIdCard(idCard: string): number | null { // 实现逻辑 }最后再分享一个小技巧如果项目里需要频繁计算年龄可以考虑用缓存。比如用一个Map把身份证号码和算出的年龄存起来下次遇到同样的号码直接返回缓存结果。但要注意年龄是会随时间变化的缓存需要设置过期时间比如每天凌晨清空一次。这个优化在数据量特别大的场景下效果很明显普通场景下用不用都行。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →