Skip to content

正则里面的 lastIndex #13

Description

@ASkyBig

同事遇到了这样一个场景,用正则进行循环判断,简化的代码大概是下面这样:

var reg = /ab/g

console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // false

这里是不是有人会觉得奇怪,为什么第二个是 false 呢。

test()

其实如果我们仔细看过MDN上关于test的描述,应该就知道了:

关于这个lastIndex,很多小伙伴可能没有听过也没有用过,让我们看看下面的代码:

var reg = /ab/g
console.log(Object.getOwnPropertyDescriptors(reg))
//{lastIndex: {value: 0, writable: true, enumerable: false, configurable: false}}

可以看到,reg 含有一个不可枚举的 lastIndex 属性。当然,如果你直接用 Object.keys(reg) 是看不到的:

var reg = /ab/g
console.log(Object.keys(reg)) // []

(知识点:getOwnPropertyDescriptors() 和 keys() 的区别是什么呢。keys() 方法会返回一个由一个给定对象的自身可枚举属性组成的数组。)
这个就是造成上面第二个是false的原因。因为每执行一次,其实我们的lastIndex都会变化:

var reg = /[ab]/g

console.log('lastIndex', reg.lastIndex) // lastIndex 0

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 1

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 2

reg.test('ab')
console.log('lastIndex', reg.lastIndex) // lastIndex 0

根据上面的代码可以看到,这个lastIndex是会循环的。

所以第二次就出了问题。如果我们想让一开始的代码正常工作,该怎么办呢?

var reg = /ab/g

console.log(reg.test('ab')) // true
reg.lastIndex = 0
console.log(reg.test('ab')) // true

我们需要在每次执行后手动设置lastIndex。

test() 应用场景

我们举一个应用场景:后端传回来了一个图片 url 的数组,有 jpg、png 各种格式的,我们需要拿到以 jpg 结尾的图片:
首先,我们看看下面的代码:

const reg = /\.jpg/g;
const arr = [
    'test0.png',
    'test1.jpg',
    'test2.jpg',
    'test3.jpg',
    'test4.jpg',
    'test5.jpg',
];
const res = arr.filter(item => reg.test(item));

(关于判断图片格式的这个正则大家不要纠结,其实是不完善的,比如url的名字是 ajpg.jpg 这种也可以匹配到,这个不重要,不是本文重点)
乍一看,觉得没毛病。我们看看结果:

WTF???
你肯定反应过来了,因为lastIndex变了,所以和预期不一致了,我们输出一下看看:

可以看到,true和false是依次输出的,所以我们没有拿到全部的jpg格式的url。
还有,就是如果当前匹配失败了,lastIndex也会重置为0,这一点也很重要,不然后面的就匹配不到了。

所以解决方法之一就是手动重置lastIndex:

其实,最好的方法就是直接去掉g,因为这种场景其实是不需要加上g的。这样lastIndex就一直是0,不会变了。结果也是符合预期的:

exec

此外,lastIndex 还和另一个正则方法 exec() 也是好兄弟,关系很近。用法和test()原理一样。
很多人分不清 execmatch 的关系,或者可能没有用过 exec
matchexec的关系:
首先,两个方法的来源是不一样的:

String.prototype.match()
RegExp.prototype.exec()

然后,不加g的时候是一样的:

然后,加了g的话:
match就没有捕获组了:

这个时候我们就需要用exec()方法了。

捕获组

关于捕获组,这里简单介绍一下。
捕获组分为四类:
捕获 ()
不捕获 (?:)
正向捕获 (?=)
反向捕获 (?!)

捕获组的功能很强大,这里就举两个例子:
例1:对日期进行任意格式的转换

var reg = /(\d{2})\/(\d{2})\/(\d{4})/
var data = '10/24/2017'
data = data.replace(reg, '$3/$1.$2')
console.log(data) //2017/10.24

例2: 千分位数字

'12345678'.replace(/(?=(\d{3})+$)/g, ',') // 12,345,678

因此,exec结合捕获组就可以实现很多复杂的逻辑。

exec() 应用场景

举个例子,我们要把一个字符串 a1b2c3d4e5里面的偶数的数字变成0。(方法肯定很多,这里只考虑用正则的方式)
肯定有同学说直接用 replace 就行了:

 'a1b2c3d4e5'.replace(/[2468]/g,0) // "a1b0c3d0e5"

(因为这里的转换条件比较简单,如果是很复杂的转换,比如我要把偶数的数字先转成ASCII码,然后反转呢?主要就是意思一下哈。)

我个人觉得exec比match强大的地方在于它可以深入到内部,暂停执行,类似 generate 函数,而不像match一样是个黑盒。

exec() 和 lastIndex

关于这个,其实业务中感觉用的不多,就举个例子吧:

var regex = /[a-z]_[a-z]/ig;
var str = 'a_b_c';
var match;

while ((match = regex.exec(str)) !== null) {
    console.log('match', match)
    regex.lastIndex--
}
// a_b
// b_c

代码大家肯定看得懂,我就不说了。match 是无法实现这样的功能的,因为匹配完就丢弃了。
使用exec() 要注意的点就是不要写出死循环(毕竟有while)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions