匹配foo -foo foo居然返回true!?弱鸡debug的辛酸经历

这是前几天在百度前端学院写的笔记。
主要内容是关于js中正则的三个匹配分组(?:)(?=)(?!)
话说js正则没有向后匹配啊…

先重复一下任务描述

  1. 编写一个判断给定数字是否为手机号码的正则表达式,测试用例参照但不限于:

    1
    2
    3
    18812011232 // 测试结果应该为 true
    18812312 // false
    12345678909 // false
  2. 编写一个判断输入的字符串是否有相邻重复单词的正则表达式,测试用例可以参考但不限于:

    1
    2
    3
    foo foo bar     // true
    foo bar foo // false 有重复单词但是不相邻
    foo barbar bar // false

关于匹配手机号码

手机号码相信大家都很轻松,最差不过\d{11};
然后匹配第一位是1的1\d{10};
匹配第二位是34578的1[34578]\d{9};
匹配第二位是3时第三位是012356789的1(3[0-35-9]|[4578]\d)\d{8};
……以此类推


关于匹配相邻重复单词

简单的单词重复匹配

首先是一个单词:[A-Za-z]+;
然后是相邻重复的字符串:([A-Za-z]+)\s+\1;
然后加上单词边界:\b([A-Za-z]+)\s+\1\b;

相信大多人都能做到这一步,接着我想起了一个问题,font-size算不算是一个单词呢?

匹配带有连字符的重复单词

先不管它算不算,匹配font-size倒是一个值得研究的问题。
所以加上可能出现的-连字符:\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b;
这里说明一下(?:)“非捕获型分组”的意思,用下面的代码说明比较直观:

1
2
"abcabc".match(/(a).*\1/g);        // ["abca"]
"abcabc".match(/(?:a)(b).*\1/g); // ["abcab"]

我们用()时就会临时保存一个分组1,使用\1就可以引用,但当我们不想保存这个分组时我们就可以使用(?:),毕竟js里我们只有9个分组可以使用。

让我们尝试一下:

1
2
3
4
var reg = /\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b/g;
"font-weight font-size font-size".match(reg); // ["font-size font-size"]
"font-size font-weight font-size".match(reg); // null
"font-size font-sizesize font-weight".match(reg); // null

不错,完美。

试试匹配各种符号乱入的单词吧

然而很快我就发现了一个严重的问题,那就是上面的正则匹配foo -foo foo或者bar bar- bar甚至aaa-aaa aaa居然能成功!

1
2
3
4
var reg = /\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b/g;
"foo -foo foo".match(reg); // ["foo foo"]
"bar bar- bar".match(reg); // ["bar bar"]
"aaa-aaa aaa".match(reg); // ["aaa aaa"]

这问题可就大了,然后我在控制台里测试了一下

1
2
"-abc".match(/.?\b.?/g);  // ["-a", "c", ""]
"def-".match(/.?\b.?/g); // ["d", "f-"]

懂了,连字符-刚好在单词边界外。除此之外,我又意识到了一个问题,假如单词边界外是乱七八糟的符号呢?

1
2
var reg = /\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b/g;
"aaa@# $%^&*aaa aaa".match(reg); // ["aaa aaa"]

呵呵

弱鸡debug的辛酸结局

仔细想想,其实只需要判断单词两边是不是空白符就好了对吧,考虑到开头和结尾,还需要加上^$的情况,所以改成(?:^|\s)\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b(?:\s|$);
让我们来试试

1
2
3
4
5
6
7
var reg2 = /(?:^|\s)\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b(?:\s|$)/g;
"aa aa".match(reg2); // ["aa aa"]
"aa-aa aa-aa".match(reg2); // ["aa-aa aa-aa"]
"aa bb aa".match(reg2); // null
"aa aa- aa".match(reg2); // null
"aa-aa aa".match(reg2); // null
"aa-aa aa bb-bb bb-bb".match(reg2); // [" bb-bb bb-bb"]

不错,这次正常了,不过仔细看很快又发现了一个问题

1
2
var reg2 = /(?:^|\s)\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b(?:\s|$)/g;
"bb aa aa bb".match(reg2); //[" aa aa "]

没错,我们是判断了单词两边是否是空白符,但同时也匹配到了单词两边的空白符,后面的空白符可以改成(?=\s|$),这里解释一下,(?=)叫做“向前正向匹配分组”,同样,我们用代码解释

1
2
"abcd".match(/ab(?=c)/g);   // ["ab"]
"abbc".match(/ab(?=c)/g); // null

当我们使用向前正向匹配时,只有之后的字符串与之匹配时才能匹配整个字符串。
当然,同样的我们还有一个叫“向前负向匹配分组”的东西(?!),跟(?=)相反,只有当之后的字符串不匹配时才能匹配整个字符串。

1
2
"abcd".match(/ab(?!c)/g);   // null
"abbc".match(/ab(?!c)/g); // ["ab"]

这是我最后得到的正则表达式:

(?:^|\s)\b([A-Za-z]+(?:-[A-Za-z]+)*)\s+\1\b(?=\s|$)


然而我还是想不到有什么好的办法解决掉前面的空格,如果有大神解决了这个问题,望告知。
最后欢迎review

作业地址 代码地址 预览地址