JS密码校验单元测试批量运行失败、单独运行通过问题排查
问题根因
问题核心是带g(全局匹配)标志的JavaScript正则表达式对象是有状态的:
所有带g修饰符的RegExp实例会维护一个内置的lastIndex属性,记录上一次匹配结束的索引位置,下次调用.test()、.exec()等匹配方法时,会从这个lastIndex位置开始向后查找,而不是从字符串起始位置匹配。如果本次查找没有命中结果,lastIndex才会被重置为0。
你代码里的passwordConditions对象是模块级全局变量,内部所有正则都带g标志,会被所有测试用例、两个导出函数共享:
- 单个测试用例单独运行时,每个正则只被调用一次,
lastIndex初始值为0,匹配结果正常。 - 批量运行测试时,前序用例执行时会修改正则的
lastIndex值,后续用例复用同一个正则实例时,匹配起点不是字符串开头,就会出现"明明存在对应字符却匹配失败"的错乱,导致提前抛出不符合预期的错误、强度计数错误。
你观察到"把passwordConditions定义在函数内部测试就正常",本质是因为每次调用函数都会创建全新的正则实例,lastIndex始终为初始值0,不存在跨调用的状态污染。
两个报错都完全符合这个问题特征:
- 校验空白字符的用例执行时,大写字母正则的
lastIndex停在之前匹配的中间位置,从该位置向后查找找不到大写字母,提前抛出缺少大写字母的错误。 passwordStrength计算强度时,部分正则因为lastIndex偏移少统计了符合条件的字符类型,导致强度等级计算偏低,出现预期返回medium实际返回weak的问题。
修复方案
三种方案都可以解决问题,优先选第一种:
- 方案1(最推荐,改动最小):移除所有正则的
g标志。你的场景只需要判断"字符串中是否存在至少一个匹配字符",不需要全局查找所有匹配项,去掉g之后正则每次匹配都会从字符串起点开始,不会保留lastIndex状态,从根源上避免状态污染。
修改后的passwordConditions如下:const passwordConditions = { "lowercase letter": /[a-z]/, "uppercase letter": /[A-Z]/, "digit ": /[0-9]/, "special character": /[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/, "whitespace character": /\s/, }; - 方案2:每次执行正则匹配前,手动将
lastIndex重置为0。需要修改两个函数的匹配逻辑,适合必须保留g标志的场景:// passwordIsValid 循环部分修改 for (const condition in passwordConditions) { const reg = passwordConditions[condition]; reg.lastIndex = 0; // 手动重置匹配起点 if (!reg.test(password)) { throw errorMessages[condition](); } } // passwordStrength 遍历部分修改 errorMessages.errorRegex().map((condition) => { condition.lastIndex = 0; // 同样手动重置 if (condition.test(password)) { strengthValue++; } }); - 方案3:保留
g标志,将passwordConditions定义在使用它的两个函数内部,每次调用函数时创建全新的正则实例,避免跨调用共享正则对象。缺点是每次函数调用都会新建正则实例,性能损耗在密码校验场景下可以忽略。
额外提示:你代码中
digit的键名末尾多了一个冗余空格,虽然当前逻辑可以正常运行,建议去掉避免后续引用键名时因为空格问题出现undefined错误。
内容的提问来源于stack exchange,提问作者project-LKH
相关产品推荐
相关产品推荐

