正则小应用:循环链表型数据

147 阅读5分钟
原文链接: zhuanlan.zhihu.com

刚做完个小项目,正准备喘口气,产品说要添加个演示模式。

问:何为演示模式?

答:不用登录,项目仍可用,但产生的数据不上传。

问:怎么启动演示模式开关?

答:顺时针点击四角固定次数后开启。

了解需求后,心想这个简单,不就是个作弊模式嘛,一会儿就搞定。

但实现的过程中想到了一些事情,并最后有了本篇文章。


方便说明,示意图如下:

其中1到4表示相应的四个区域。html脚本示意如下:

<div data-index="1" class="tap-area">1</div>
<div data-index="2" class="tap-area">2</div>
<div data-index="3" class="tap-area">3</div>
<div data-index="4" class="tap-area">4</div>

最开始的脚本代码是:

document.querySelectorAll('.tap-area').forEach(v => v.onclick = handleClick)

let array = []
let total = 8

function handleClick() {
	array.push(this.dataset.index)
	
	if (array.length > total) {
		array.shift()
	}
	console.log(array)
	
	if (check(array)) {
		console.log('进入演示模式')
		array = []
	}
}

function check(array) {
	if (array.length < total) return false
	return '12341234' === array.join('')
}

上面的代码很简单,为四个角添加事件,每次点击时把其代表的数字放进array数组中。并且都check数组是否已满足开启条件。

其核心是check的实现,我最开始想到的点击顺序是:用户从右上角顺时针点击两圈就可以了,即12341234就能满足需求了。

如果没有其他的想法,事情也就到此为止了。


后来想了一想,不管起点是啥,只要是顺时针即可,我该如何实现?也觉得这样比较合理一些。

首先我知道最傻的办法是:

function check(array) {
	if (array.length < total) return false
	
	let string = array.join('')
	return string === '12341234' ||
		string === '23412341' ||
		string === '34123412' ||
		string === '41234123'
}

当然,觉得太low了,改用正则:

function check(array) {
	if (array.length < total) return false
	return /12341234|23412341|34123412|41234123/.test(array.join(''))
}

可以优化一下:

function check(array) {
	if (array.length < total) return false
	return /(1234|2341|3412|4123)\1/.test(array.join(''))
}

其中正则使用了反向引用\1,表示前面括号里匹配到的内容。注意此正则不可以写成:

/(1234|2341|3412|4123){2}/

其实,它并不等价于:

/(1234){2}|(2341){2}|(3412){2}|(4123){2}/

因为前者还匹配如下的字符串:

/(1234|2341|3412|4123){2}/.test('12342341') // true

到这里,我又想到,如果需求是顺时针走三圈的话,就可以改成:

/(1234|2341|3412|4123)\1{2}/

突然脑海里进来一个恶魔,如果是两圈半呢?比如 '1234123412' 这样的数据。最开始想到的是正则:

/((12)34|(23)41|(34)12|(41)23)\1\2/

后来发现匹配其他数据,比如 '2341234123' 这个,就不可以。因为其中 \2 始终指向第二个括号里匹配的数据,即(12)。因此要匹配它,正则应该是:

/((12)34|(23)41|(34)12|(41)23)\1\3/.test('2341234123') // true

如果要把四种情况考虑进来的话,那还不如下面的这种方式来得直接呢:

/1234123412|2341234123|3412341234|4123412341/

至此我对于自己的思考还挺满意。正洋洋得意时,脑海里又进来一个恶魔,假如平板不是四个角呢?!


虽然现实的平板或者web页面都是方形的。

但真假设是5个角的话,正则改写为:

/(12345|23451|34512|45123|51234)\1/

当然,一打眼就知道肯定不是这么扩展的。此时我想到了循环链表。

12345和23451应该是同一种数据类型,只是表头不一样罢了。

难道要搞一个 helper 函数去判断 12345 和 23451 是否是同类数据?


正琢磨际,来了灵感,这种循环数据不就是无限轮播嘛。

记得有一种实现无缝滚动的方式是把dom拷贝成两份,进而滚动过程中就不用移动dom了。

12345重复3次,即:

123451234512345

是包含23451的:

1,2345123451,2345

上面的数据是目标数据,应该用正则 /2345123451/ 去匹配它,即:

正则是变的,而目标字符串是故定的。于是有了下面的写法:

function check(array) {
	if (array.length < total) return false
	return new RegExp(array.join('')).test('123412341234')
}

此时,为自己能想到这种方法感到十分兴奋,突然我又意识到了一个问题:这不就是查找嘛!

为啥还要用正则???

function check(array) {
	if (array.length < total) return false
	return '1234'.repeat(3).indexOf(array.join('')) != -1
}

也可以修改成如下:

let string = '1234'
function check(array) {
	if (array.length < total) return false
	return string.repeat(2 * total / string.length).indexOf(array.join('')) != -1
}

2.5圈也没问题了。


想起了一句话:当你手里拿着锤子时,看什么都像钉子。

也欢迎看《JS 正则迷你书》

本文完。