正则表达式的全局搜索标志g导致表单验证反复成功失败

0x01 遇到了一个BUG

这周遇到了一个很有意思的问题,记录一下。

背景是项目编写完成后,我着手开始优化form表单的正则验证。当使用封装好的正则验证类验证手机号时,在change中会出现一次成功一次失败的问题。

这里上一下我复盘时写的demo代码来做展示:

  • App.vue

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    <template>
    <div id="app">
    <el-form :model="testForm" :rules="testFormRules" label-width="150px" :style="{ width: '400px', margin: '0 auto' }">
    <el-form-item prop="num" label="正则验证数字">
    <el-input v-model="testForm.num"></el-input>
    </el-form-item>
    <el-form-item prop="num1" label="自定义方法验证数字">
    <el-input v-model="testForm.num1"></el-input>
    </el-form-item>
    </el-form>
    </div>
    </template>

    <script>
    import { RegExpTable } from './RegExpTable'
    export default {
    name: 'App',
    data () {
    return {
    testForm: {
    num: '',
    num1: ''
    },
    testFormRules: {
    num: [
    {
    required: true,
    pattern: RegExpTable.numRegex,
    message: '请输入数字',
    trigger: 'change'
    }
    ],
    num1: [
    {
    required: true,
    validator: (rule, value, callback) => {
    if (!RegExpTable.numRegex.test(value)) {
    console.log(`error pattern = ${RegExpTable.numRegex} value = ${value}`)
    return callback(new Error('请输入数字'))
    } else {
    console.log(`success pattern = ${RegExpTable.numRegex} value = ${value}`)
    callback()
    }
    },
    trigger: 'change'
    }
    ]
    }
    }
    }
    }
    </script>
  • RegExpTable.js

    1
    2
    3
    export const RegExpTable = {
    numRegex: /^[0-9]+$/g
    }

num1的输入框中输入数字1验证

就会出现一次成功一次失败的问题,而在num输入框中则不会遇到问题。

0x02 修复BUG

发现这个问题的时候项目已经临近deadline了,没有足够的时间让我找出具体原因来对症下药,只能尽快求稳的修复这个BUG。

明面上的解决办法很简单,既然num输入框设置正则表达式能正常使用,那说明我在自定义验证方法中直接使用正则表达式(而非引用)应该也能使用,于是我把代码改成这样:

1
2
3
4
5
6
7
8
9
// App.vue
// 把RegExpTable.numRegex直接替换为正则表达式
if (!/^[0-9]+$/g.test(value)) {
console.log(`error pattern = ${RegExpTable.numRegex} value = ${value}`)
return callback(new Error('请输入数字'))
} else {
console.log(`success pattern = ${RegExpTable.numRegex} value = ${value}`)
callback()
}

运行测试,果然可以使用:

改到这里时我在想,那是不是可以使用new RegExp来构造正则表达式,在RegExpTable.js中存储每个正则表达式的字符串呢?

于是我又做出了一些改动:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// RegExpTable.js
// 改成 正则字符串
export const RegExpTable = {
numRegex: '^[0-9]+$'
}

// App.vue
// 在num1的自定义方法中使用RegExp构造正则表达式
const regex = new RegExp(RegExpTable.numRegex, 'g')
if (!regex.test(value)) {
console.log(`error pattern = ${RegExpTable.numRegex} value = ${value}`)
return callback(new Error('请输入数字'))
} else {
console.log(`success pattern = ${RegExpTable.numRegex} value = ${value}`)
callback()
}

测试了下,方法可行。

0x03 为什么这样写会出BUG

虽然赶在deadline前准时完工了,但这个问题依旧困扰着我。

这周末有空就翻阅了下MDN,终于知道了为什么。

在MDN关于 RegExp.prototype.test() 的文档中,有这么一段话

如果正则表达式设置了全局标志,test()的执行会改变正则表达式lastIndex属性。连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串

同时也给出了一个例子

1
2
3
4
5
6
7
var regex = /foo/g;

// regex.lastIndex is at 0
regex.test('foo'); // true

// regex.lastIndex is now at 3
regex.test('foo'); // false

!!!,这与我遇到的问题正好一致啊

RegExp.lastIndex文档中提到**lastIndex** 是正则表达式的一个可读可写的整型属性,用来指定下一次匹配的起始索引。且只有在正则表达式使用了表示全局检索的 “g“ 标志时,该属性才会起作用。

关于它的规则是这样的:

  • 如果 lastIndex 大于字符串的长度,则 regexp.testregexp.exec 将会匹配失败,然后 lastIndex 被设置为 0。
  • 如果 lastIndex 等于字符串的长度,且该正则表达式匹配空字符串,则该正则表达式匹配从 lastIndex 开始的字符串。(then the regular expression matches input starting at lastIndex.)
  • 如果 lastIndex 等于字符串的长度,且该正则表达式不匹配空字符串 ,则该正则表达式不匹配字符串,lastIndex 被设置为 0.。
  • 否则,lastIndex 被设置为紧随最近一次成功匹配的下一个位置。

所以正是因为lastIndex属性值的存在,导致每次验证不一定是从第一个字符开始匹配的而出现的问题。
所以当我每次都构建正则表达式后再验证时,就不会遇到验证失败的问题了。

因为全局搜索标志的作用是从上一个匹配的位置继续进行搜索,而我这里执行的是一次性的格式验证。

此时,这个问题的最优解就成了:去掉全局搜索标志g

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// RegExpTable.js
export const RegExpTable = {
numRegex: /^[0-9]+$/
}

// 自定义验证方式
(rule, value, callback) => {
if (!RegExpTable.numRegex.test(value)) {
console.log(`error pattern = ${RegExpTable.numRegex} value = ${value}`)
return callback(new Error('请输入数字'))
} else {
console.log(`success pattern = ${RegExpTable.numRegex} value = ${value}`)
callback()
}
}


评论区