正则字符串


概述

正则表达式是一段特殊规则的字符串,它可以在一个字符串中找出满足指定规则的字符串子集,在处理字符串时,正则表达式有非常强的能力。

比如处理一个日期字符串,我们需要分离其年月日,如下:

var date = "2000-01-02";
var arr = date.split("-");
console.log("年", arr[0]);
console.log("月", arr[1]);
console.log("日", arr[2]);

但是现在出现了新的情况,数据格式不严谨,数据可能是以下样子:

var dates = ["2000-01-02", "2000/01/02", "2000.01.02", "2000-1-2"];

此时针对不同类型的日期格式,我们需要使用不同的方式去分割,并且在此之前,我们可能还不知道它们具体是什么格式,此时,一个正则字符串解决上述所有问题:

var dates = ["2000-01-02", "2000/01/02", "2000.01.02", "2000-1-2"];

for (var i = 0; i < dates.length; i++) {
  var m = dates[i].match(/^(\d{4}).(\d{1,2}).(\d{1,2})$/);
  if (m !== null) {
    console.log("年", m[1]);
    console.log("月", m[2]);
    console.log("日", m[3]);
  }
}

在上面代码中,/^(\d{4}).(\d?\d).(\d?\d)$/ 这一串火星文就是正则字符串,它表示一种规则,当字符串满足此规则,它会对其进行截取和分割。

现在简单的了解下这段正则字符串:

/^(\d{4}).(\d?\d).(\d?\d)$/;
  • 首尾的反斜杠 / 是用来声明正则字符串,就类似普通字符串的单双引号。
  • 开头的上尖号 ^ 指定字符串必须按照后续规则开头,也就是限制字符串的开头。
  • 末尾的 $ 和上尖号 ^ 类似,表示了必须按照这个规则结尾,也就是限制字符串的结尾。
  • \d{4} 表示四个连续的数字,这个规则对应了年。
  • 圆括号用来包裹一组规则,表示这段规则记作一组。正则字符串会在查找后把每组规则内的字符串会依次分割出来,所以后续我们可以根据数组访问 m[1]m[2]m[3] ,对应了年月日三组数据。
  • 小数点 . 表示任何字符,正好对应了我们日期分隔符的不同。
  • \d{1,2} 表示连续 1 到 2 个数字,对应了日期的月日缺省前缀 0 的情况,比如 dates 的最后一个值。

把上面的话合并一下,这串正则表达式的意思就是:以 4 个数字开头,连接任意一个字符,连接一个或者两个数字,连接任意一个字符,连接一个或者两个数字结尾的字符串。

实际上正则表达式真的真的很简单,希望大家学习本课程千万不要有负担,不要被如火星文一般的表示方式迷惑,只要学习完正则的语法,它完全是可读的

备注:大部分程序语言的正则字符串都是统一参照 PCRE 标准,JS 中也是,所以你学习完正则字符串后,以后也可以在其他语言使用。如果你已经学习过 PCRE 标准的正则,那么可以直接跳过学习部分,去看使用章节。

备注:为了简化,正则字符串后面简化为正则。

创建与 match 函数

正则的创建有两种方式:

  • 字面量创建:使用反斜杠直接包裹
  • 构造函数创建:使用 RegExp 构造函数
var reg1 = /12345/;
var reg2 = new RegExp("12345");

上面两种方式都可以创建正则,第一种是字面量创建,第二种是函数创建,RegExp 函数接受一个普通字符串当做正则的规则。从第二种方式可以看出,正则在 JS 中是引用类型,它不是字符串类型。

每个字符串都可以使用 match 函数来根据正则查找,match 函数存在于 String.prototype 上。它的使用如下:

var m = "abc 123 ABC".match(/123/);
console.log(m);

match 函数会根据正则查找字符串,然后返回查找结果,如果不成功,返回一个 null

备注:在正则中,把查找的过程称作匹配,后续介绍中,都会使用匹配一词代替查找。

规则

普通匹配

普通匹配就如同查找字符串,比如:

var foo = "abc 123 ABC";
var m = foo.match(/123/);
console.log(m);

正则 /123/ 的规则是字符串 '123',上面代码中,会在 foo 字符串中查找字符串 '123',如果匹配成功,返回匹配的字符串。

大部分字符在正则中都没规则意义,它们用于指定匹配的内容,比如数字,大小写字母,汉字等。

特殊规则字符

在正则中,一部分字符或者转义字符具有特殊的意义,它们表示一定的规则,以下是常用具有规则意义的字符:

规则字符描述
^匹配字符串的开头,用以决定字符串由什么规则开始,比如 /^abc/,可以匹配 abcdefe,但是不能匹配 1abcd
$匹配字符串的结尾,用以决定字符串由什么规则结束,比如 /abc$/,可以匹配 123abc,但不能匹配 abcd。当它和 ^ 配合使用时,就限制了整个字符串的开头的结尾。
.匹配任何字符,比如 /a.c/ 就可以匹配到 123aac,中的 aac,或者匹配到 a_b456 中的 a_b
*匹配前一个规则 0 次或多次,比如 /a*b/ 可以匹配字符串中的 babaaaab
+匹配前一个规则 1 次或多次,比如 /a+b/ 可以匹配字符串中的 abaaaaaab,但是不能匹配到 b,因为 b 中没有出现 a 字符
?匹配前一个规则 0 次或 1 次,比如 /a?b/ 可以匹配字符串中的 bab,但不能匹配到 abb
\d匹配一个数字,比如 /1\d\d/ 可以匹配字符串中的 123100,例如在 ab19876 中,可以匹配到 198 三个数字。
\D匹配一个非数字,比如 /\D\d/ 可以匹配字符串中的 h1h2c0,例如在 123@123.com 中,可以匹配到 @1 这两个字符。
\s匹配一个空白符,比如换行空格【tab】制表符,比如 /abc\s/ 可以匹配 abc abc! 中的 abc ,而不能匹配 abc!
\S匹配一个非空白符,比如 /abc\S/ 可以匹配 abc abc! 中的 abc!
\w匹配大小写字母数字和下划线,比如 /abc\w123/ 可以匹配字符串中的 abc_123abcA123,但是不能匹配abc@123abc!123
\W匹配大小写字母数字和下划线之外的字符,比如 /abc\w123/ 可以匹配字符串中的 abc@123abc!123
\n匹配换行符。
\r匹配回车符。
\t匹配水平制表符。
\转义字符,可以把特殊字符转义为普通字符,比如需要匹配 JS 的文件,可以使用 /.+\.js$/,它会匹配如 app.jspro.js 等字符串。同理,\^$*+?{}[] 等字符也可以使用正斜杠转义。
\b匹配单词边界,比如 /\bpen\b/ 只能能够单词 pen,例如 a pen,a box 中可以匹配 penpen 的左边是空格,右边是逗号,一般很难处理,但是使用 \b 就可以很容易匹配。
{n}匹配前一个规则 n 次,比如 /fo{2}/ 可以匹配字符串中的 foo,例如在 bars foos 中,foo 将会被匹配。
{n,}匹配前一个规则多次,但是至少 n 次,比如 /fo{2,}/ 可以匹配 foofooofooooo,其中字符 o 不能匹配 2 次以下。
{n,m}匹配前一个规则 n 到 m 次,比如 /fo{2,3}/ 就只能匹配 foo、与 fooo
x\|y匹配规则 x 或者规则 y,比如 /1a\|b1/ 可以匹配 1a1 或者 1b1
[xyz]一个规则集合,匹配其中的任意规则,比如:/1[abc]1/ 可以匹配 1a11b11c1。在规则集合中,可以使用扩折号来指定字母或者数字范围,比如 /[a-z0-9A-Z_]/ 就等同于 /\w/
[^xyz]匹配一个规则集合的补集,比如:/1[^abc]1/ 可以匹配 1!11@1,但是不能匹配 1a1
(x)匹配一个模式,可以理解成一个规则组。比如 /(ab){2}/ 可以匹配 abab,如果不使用圆括号,{} 只能修饰前一个字符 b,而不是 ab。模式可以配合其他特殊字符使用,比如 /(fo*){3}/ 可以匹配 ffffoffoofoofooofoooo
\数字匹配一个模式的结果,数字指定了匹配哪一个模式,比如 /(\d)a\1/ 中,\1表示匹配第一个模式的结果,其中第一个模式就是 (\d),所以 /(\d)a\1/ 可以匹配诸如 1a19a9 这样的字符串,但是不能匹配 1a22a7,因为 \1 匹配的是模式的结果,不是模式的规则。
(?:x)匹配一个模式,但是不记录该模式,比如 /(?:foo)(bar) \1/ 中, \1 找到的第一个模式将会是 (bar),而不是 (?:foo)(?:foo) 的作用和 (foo) 一样,但是它不被记录。
x(?=y)匹配规则 x,但是需要规则 xy 成立。比如 /foo(?=bar)/,对于字符串 foo foobar,它会匹配第二个 foo,因为 foo 后必须要有 bar 字符串。
(?<=y)x匹配规则 x,但是需要规则 yx 成立。比如 /(?<=foo)bar/,对于字符串 bar foobar,它只会匹配第二个 bar,因为 bar 前面必须要有 foo 字符串。
x(?!y)匹配规则 x,但是需要规则 xy 不成立。比如 /foo(?!bar)/,对于字符串 foobar foo,他只会匹配第二个 foo,因为 foo 后面不能有 bar 字符串。
(?<!y)x匹配规则 x,但是需要规则 yx 不成立。写烦了不写例子了,前三个规则和这个规则使用并不频繁,需要使用的时候查表即可。

以上是大部分正则中的特殊字符,使用较频繁。但是还没有列举完全,部分特殊字符使用非常少,可以 点此查表

贪婪模式

默认情况下,JS 中的正则匹配是贪婪模式,贪婪模式会尽可能多的匹配字符串。在使用 *+{n,} 时,它们会尽可能多的匹配,比如:

var re = "foo 123 foo abc foo zzz foo".match(/foo.*foo/);
console.log(re);

匹配的结果是整个字符串,从开头的 foo 到结尾的 foo 中间,都会被 .* 匹配。这种情况的匹配,称作贪婪模式。

可以在 *+{n,} 后面使用 ? 来解除贪婪模式,让其匹配最短字符串,比如:

var re = "foo 123 foo abc foo zzz foo".match(/foo.*?foo/);
console.log(re);

上面的正则只会匹配 foo 123 foo,然后就停止匹配。

模式修正符

正则匹配时,还区分大小写,匹配成功的时候也会停止匹配,这些规则是可以调整的,使用讴歌 模式修正符 可以修改正则的匹配模式,模式修正符被放到正则的末尾,反斜杠 /的后面。

JS 中有常用模式修正符如下:

修正符描述
g全局匹配,不会在匹配成功后停止匹配。
i忽略大小写,比如 /abc/i 此时可以匹配到 ABCAbcaBC 等。
m多行匹配,配合 ^$ 使用,可以匹配任何一行的开头和结尾。比如 /^bar/ 不能匹配 foo\nbar 中的 bar 字符串,但是 /^bar/m 就可以匹配,因为 \n 换行了,bar 字符串是第二行的开头。
s. 可以匹配换行符,默认情况下 /.*/ 只能匹配 foo\nbarfoo,不匹配换行符,但是 /.*/s 就可匹配整个字符串 foo\nbar

如果使用 RegExp 创建正则,模式修正符放在第二个参数:

var reg = new RegExp(".*", "s");
var re = "foo\nbar".match(reg);

模式修正符还有两个,它们可能并不常有或者是新标准的规定,我不会对其介绍,如果你非常非常有兴趣,可以看看:

修正符描述
u是否开启 Unicode 模式匹配。Unicode 是一种字符集,开启此模式可以按一定模式匹配 Unicode 字符。比如中文 '' 的 Unicode 编码是 662f,当我们匹配中文 '' 这个字的时候,可以使用 '是'.match(/\u{662f}/u) 来匹配,可以匹配到中文 '' 这个字。
如果不开启 Unicode 模式,代码 '是'.match(/\u{662f}/) 会得到一个 null\u{xxxx} 这种写法不生效。一般情况下,很少使用这种写法,因为 :
- '是'.match(/\u662f/)
- '是'.match(/是/)
- '\u662f'.match(/是/)
- '\u662f'.match(/\u662f/)
四种写法都可以匹配中文的 '' 字
y点击这里

使用

RegExp

在 JS 中,正则是引用类型,它们由 RegExp 构造而来,创建一个正则有以下方式:

var reg1 = /foo/i;
// or
var reg2 = new Regexp("foo", "i");
// or
var reg2 = new Regexp(/foo/, "i");

RegExp 有两个常用的函数:

函数描述
exec匹配一段字符串,匹配成功返回正则匹配结果,失败返回 null。类似字符串的 match 函数
test匹配一段字符串,是返回 true,反之 false
var reg = /\w+@\w+\./;
var htmlStr = "<p> <span>hello</span> </p>";
console.log(reg.exec(htmlStr));
console.log(reg.test(htmlStr));

其中 exec 函数在匹配成功时,返回的对象是一个数组,它的下标和属性对应以下结果:

下标或属性描述
0匹配的字符串
1~n各个模式中匹配的字符串
index匹配字符串的下标
input原始字符串

字符串函数

字符串的一部分函数比如查找,替换等,也支持正则字符串:

函数描述
match匹配一个正则,成功返回匹配结果,失败返回 null。当设置全局匹配时,match 只会得到一个包含所有匹配到的字符串的数组。
matchAll匹配一个含有全局匹配的正则,返回一个迭代对象。成功或者失败都会返回一个迭代对象。(迭代对象属于新标准中的知识,再次不多介绍)
search按照正则查找字符串,函数功能不变。
replace按照规则替换字符串,函数功能不变。
split按照规则分割字符串,函数功能不变。

演示:

var str = "foobarfoobarfoobar";
var reg = /(oo)../g;

console.log(str.match(reg));

// 迭代对象可以使用 for of 遍历,for of 也是新标准中的语法,本系列课程不介绍
var mAll = str.matchAll(reg);
for (var item of mAll) {
  console.log(item);
}

console.log(str.search(reg));
console.log(str.replace(reg, " replace "));
console.log(str.split(reg));

性能与应用

性能

使用正则字符串实际上非常耗费性能,在一对多的服务中,很少使用正则,比如服务器,大量正则的使用是给计算机带来很大负担。但是 JS 是客户端语言,负担会小很多。

应用

正则一般都是用来处理有一定规则的文本,比如日期,邮箱,网址,手机号等等,使用正则分解这些文本会变得异常简单,比如本页最开始的段落,我们处理日期一样。

尝试编写一个匹配邮箱的正则:邮箱的规则一般分为两部分,@ 符前面表示账户,@ 符后表示邮箱的主机(域名),比如 QQ 邮箱就是 xxxxx@qq.com,其中账户或者主机一般都由数字、字母、下划线和扩折号 - 组成,主机至少由两个单词组成,中间用点连接。所以一个邮箱的规则可以写成:

var emailReg = /^[\w-]+@[\w-]+(\.[\w-]+)+$/;
console.log(
  emailReg.test("123456@163.com"), //true
  emailReg.test("123456@gmail.com"), // true
  emailReg.test("123456@qq.com"), // true
  emailReg.test("123456@mail.com.cn"), // true
  emailReg.test("@main.com"), // false
  emailReg.test("123456@com"), // false
  emailReg.test("123456mail.com") // false
);

如果你的项目有验证邮箱的需要,那么可以根据上述方式来判断表单是否提交,比如:

<form id="form">
  <p>申请账号需要一个邮箱</p>
  <input id="mail" type="text" name="mail" />
  <button>确定</button>
</form>
<script>
  var emailReg = /^[\w-]+@[\w-]+(\.[\w-]+)+$/;

  var formEl = document.getElementById("form");
  var inputEl = document.getElementById("mail");

  formEl.addEventListener("submit", function (e) {
    if (emailReg.test(inputEl.value)) {
      // 提交表单
      alert("验证成功,即将提交。");
    } else {
      alert("邮箱格式不正确,请重新输入。");
      e.preventDefault();
    }
  });
</script>