一.正则表达式的四种操作

正则表达式是匹配模式,不管如何使用正则表达式,万变不离其宗,都需要先“匹配”。
有了匹配这一基本操作后,才有其他的操作:验证、切分、提取、替换。
进行任何相关操作,也需要宿主引擎相关 API 的配合使用。当然,在 JavaScript 中,相关 API 也不多。

验证

验证是正则表达式最直接的应用,比如表单验证。
在说验证之前,先要说清楚匹配是什么概念。
所谓匹配,就是看目标字符串里是否有满足匹配的子串。因此,“匹配”的本质就是“查找”。
有没有匹配,是不是匹配上,判断是否的操作,即称为“验证”。
这里举一个例子,来看看如何使用相关 API 进行验证操作的。
比如,判断一个字符串中是否有数字。
使用 search:

  1. var regex = /\d/;
  2. var string = "abc123";
  3. console.log( !!~string.search(regex) );
  4. // => true

使用 test:

  1. var regex = /\d/;
  2. var string = "abc123";
  3. console.log( regex.test(string) );
  4. // => true

使用 match:

  1. var regex = /\d/;
  2. var string = "abc123";
  3. console.log( !!string.match(regex) );
  4. // => true

使用 exec:

  1. var regex = /\d/;
  2. var string = "abc123";
  3. console.log( !!regex.exec(string) );
  4. // => true

其中,最常用的是 test。

切分

匹配上了,我们就可以进行一些操作,比如切分。
所谓“切分”,就是把目标字符串,切成一段一段的。在 JavaScript 中使用的是 split。
比如,目标字符串是 “html,css,javascript”,按逗号来切分:

  1. var regex = /,/;
  2. var string = "html,css,javascript";
  3. console.log( string.split(regex) );
  4. // => ["html", "css", "javascript"]

又比如,如下的日期格式:
2017/06/26 2017.06.26 2017-06-26
可以使用 split “切出”年月日:

  1. var regex = /\D/;
  2. console.log( "2017/06/26".split(regex) );
  3. console.log( "2017.06.26".split(regex) );
  4. console.log( "2017-06-26".split(regex) );
  5. // => ["2017", "06", "26"]
  6. // => ["2017", "06", "26"]
  7. // => ["2017", "06", "26"]

提取

虽然整体匹配上了,但有时需要提取部分匹配的数据。
此时正则通常要使用分组引用(分组捕获)功能,还需要配合使用相关 API。
这里,还是以日期为例,提取出年月日。注意下面正则中的括号:
使用 match:

  1. var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  2. var string = "2017-06-26";
  3. console.log( string.match(regex) );
  4. // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

使用 exec:

  1. var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  2. var string = "2017-06-26";
  3. console.log( regex.exec(string) );
  4. // =>["2017-06-26", "2017", "06", "26", index: 0, input: "2017-06-26"]

使用 test:

  1. var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  2. var string = "2017-06-26";
  3. regex.test(string);
  4. console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
  5. // => "2017" "06" "26"

使用 search:

  1. var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  2. var string = "2017-06-26";
  3. string.search(regex);
  4. console.log( RegExp.$1, RegExp.$2, RegExp.$3 );
  5. // => "2017" "06" "26"

使用 replace:

  1. var regex = /^(\d{4})\D(\d{2})\D(\d{2})$/;
  2. var string = "2017-06-26"; var date = [];
  3. string.replace(regex, function (match, year, month, day) {
  4. date.push(year, month, day);
  5. });
  6. console.log(date);
  7. // => ["2017", "06", "26"]

其中,最常用的是 match。

替换

找,往往不是目的,通常下一步是为了替换。在 JavaScript 中,使用 replace 进行替换。
比如把日期格式,从 yyyy-mm-dd 替换成 yyyy/mm/dd:

  1. var string = "2017-06-26";
  2. var today = new Date( string.replace(/-/g, "/") );
  3. console.log( today );
  4. // => Mon Jun 26 2017 00:00:00 GMT+0800 (中国标准时间)

这里只是简单地应用了一下 replace。但,replace 方法很是强大的,是需要重点掌握的。

二.相关API注意要点

从上面可以看出用于正则操作的方法,共有 6 个,字符串实例 4 个,正则实例 2 个:
String#search String#split String#match String#replace RegExp#test RegExp#exec

search 和 match 的参数问题

我们知道字符串实例的那 4 个方法参数都支持正则和字符串。
但 search 和 match,会把字符串转换为正则的。

  1. var string = "2017.06.27";
  2. console.log( string.search(".") );
  3. // => 0 //需要修改成下列形式之一
  4. console.log( string.search("\\.") );
  5. console.log( string.search(/\./) );
  6. // => 4
  7. // => 4
  8. console.log( string.match(".") );
  9. // => ["2", index: 0, input: "2017.06.27"]
  10. //需要修改成下列形式之一
  11. console.log( string.match("\\.") );
  12. console.log( string.match(/\./) );
  13. // => [".", index: 4, input: "2017.06.27"]
  14. // => [".", index: 4, input: "2017.06.27"]
  15. console.log( string.split(".") );
  16. // => ["2017", "06", "27"]
  17. console.log( string.replace(".", "/") );
  18. // => "2017/06.27"

match返回结果的格式问题

match 返回结果的格式,与正则对象是否有修饰符 g 有关。

  1. var string = "2017.06.27";
  2. var regex1 = /\b(\d+)\b/;
  3. var regex2 = /\b(\d+)\b/g;
  4. console.log( string.match(regex1) );
  5. console.log( string.match(regex2) );
  6. // => ["2017", "2017", index: 0, input: "2017.06.27"]
  7. // => ["2017", "06", "27"]

没有 g,返回的是标准匹配格式,即,数组的第一个元素是整体匹配的内容,接下来是分组捕获的内容,然 后是整体匹配的第一个下标,最后是输入的目标字符串。
有 g,返回的是所有匹配的内容。
当没有匹配时,不管有无 g,都返回 null。

exec 比 match 更强大

当正则没有 g 时,使用 match 返回的信息比较多。但是有 g 后,就没有关键的信息 index 了。
而 exec 方法就能解决这个问题,它能接着上一次匹配后继续匹配:

  1. var string = "2017.06.27";
  2. var regex2 = /\b(\d+)\b/g;
  3. console.log( regex2.exec(string) );
  4. console.log( regex2.lastIndex);
  5. console.log( regex2.exec(string) );
  6. console.log( regex2.lastIndex);
  7. console.log( regex2.exec(string) );
  8. console.log( regex2.lastIndex);
  9. console.log( regex2.exec(string) );
  10. console.log( regex2.lastIndex);
  11. // => ["2017", "2017", index: 0, input: "2017.06.27"]
  12. // => 4
  13. // => ["06", "06", index: 5, input: "2017.06.27"]
  14. // => 7
  15. // => ["27", "27", index: 8, input: "2017.06.27"]
  16. // => 10
  17. // => null
  18. // => 0

其中正则实例 lastIndex 属性,表示下一次匹配开始的位置。
比如第一次匹配了 “2017”,开始下标是 0,共 4 个字符,因此这次匹配结束的位置是 3,下一次开始匹配 的位置是 4。
从上述代码看出,在使用 exec 时,经常需要配合使用 while 循环:

  1. var string = "2017.06.27";
  2. var regex2 = /\b(\d+)\b/g;
  3. var result;
  4. while ( result = regex2.exec(string) ) {
  5. console.log( result, regex2.lastIndex );
  6. }
  7. // => ["2017", "2017", index: 0, input: "2017.06.27"] 4
  8. // => ["06", "06", index: 5, input: "2017.06.27"] 7
  9. // => ["27", "27", index: 8, input: "2017.06.27"] 10

修饰符 g,对 exex 和 test 的影响

上面提到了正则实例的 lastIndex 属性,表示尝试匹配时,从字符串的 lastIndex 位开始去匹配。
字符串的四个方法,每次匹配时,都是从 0 开始的,即 lastIndex 属性始终不变。
而正则实例的两个方法 exec、test,当正则是全局匹配时,每一次匹配完成后,都会修改 lastIndex。下面 让我们以 test 为例,看看你是否会迷糊:

  1. var regex = /a/g;
  2. console.log( regex.test("a"), regex.lastIndex );
  3. console.log( regex.test("aba"), regex.lastIndex );
  4. console.log( regex.test("ababc"), regex.lastIndex );
  5. // => true 1
  6. // => true 3
  7. // => false 0

注意上面代码中的第三次调用 test,因为这一次尝试匹配,开始从下标 lastIndex,即 3 位置处开始查 找,自然就找不到了。
如果没有 g,自然都是从字符串第 0 个字符处开始尝试匹配:

  1. var regex = /a/;
  2. console.log( regex.test("a"), regex.lastIndex );
  3. console.log( regex.test("aba"), regex.lastIndex );
  4. console.log( regex.test("ababc"), regex.lastIndex );
  5. // => true 0
  6. // => true 0
  7. // => true 0

test 整体匹配时需要使用 ^ 和 $

这个相对容易理解,因为 test 是看目标字符串中是否有子串匹配正则,即有部分匹配即可。
如果,要整体匹配,正则前后需要添加开头和结尾:

  1. console.log( /123/.test("a123b") );
  2. // => true
  3. console.log( /^123$/.test("a123b") );
  4. // => false
  5. console.log( /^123$/.test("123") );
  6. // => true

split 相关注意事项

split 方法看起来不起眼,但要注意的地方有两个的。
第一,它可以有第二个参数,表示结果数组的最大长度:

  1. var string = "html,css,javascript";
  2. console.log( string.split(/,/, 2) );
  3. // =>["html", "css"]

第二,正则使用分组时,结果数组中是包含分隔符的:

  1. var string = "html,css,javascript";
  2. console.log( string.split(/(,)/) );
  3. // =>["html", ",", "css", ",", "javascript"]

replace 是很强大的

总体来说 replace 有两种使用形式,这是因为它的第二个参数,可以是字符串,也可以是函数。
当第二个参数是字符串时,如下的字符有特殊的含义:
属性 描述
$1,$2,…,$99 匹配第 1-99 个 分组里捕获的文本
$& 匹配到的子串文本
$` 匹配到的子串的左边文本
$’ 匹配到的子串的右边文本
$$ 美元符号
例如,把 “2,3,5”,变成 “5=2+3”:

  1. var result = "2,3,5".replace(/(\d+),(\d+),(\d+)/, "$3=$1+$2"); console.log(result);
  2. // => "5=2+3"

又例如,把 “2,3,5”,变成 “222,333,555”:

  1. var result = "2,3,5".replace(/(\d+)/g, "$&$&$&");
  2. console.log(result);
  3. // => "222,333,555"

再例如,把 “2+3=5”,变成 “2+3=2+3=5=5”:

  1. var result = "2+3=5".replace(/=/, "$&$`$&$'$&");
  2. console.log(result);
  3. // => "2+3=2+3=5=5"

我们对最后这个进行一下说明。要把 “2+3=5”,变成 “2+3=2+3=5=5”,其实就是想办法把 = 替换成 =2+3=5=,其中,$& 匹配的是 =, $匹配的是 2+3,$' 匹配的是 5。因此使用 "$&$$&$’$&” 便达成了 目的。
当第二个参数是函数时,我们需要注意该回调函数的参数具体是什么:

  1. "1234 2345 3456".replace(/(\d)\d{2}(\d)/g, function (match, $1, $2, index, input) {
  2. console.log([match, $1, $2, index, input]);
  3. });
  4. // => ["1234", "1", "4", 0, "1234 2345 3456"]
  5. // => ["2345", "2", "5", 5, "1234 2345 3456"]
  6. // => ["3456", "3", "6", 10, "1234 2345 3456"]

此时我们可以看到 replace 拿到的信息,并不比 exec 少。

使用构造函数需要注意的问题

一般不推荐使用构造函数生成正则,而应该优先使用字面量。因为用构造函数会多写很多 \。

  1. var string = "2017-06-27 2017.06.27 2017/06/27";
  2. var regex = /\d{4}(-|\.|\/)\d{2}\1\d{2}/g;
  3. console.log( string.match(regex) );
  4. // => ["2017-06-27", "2017.06.27", "2017/06/27"]
  5. regex = new RegExp("\\d{4}(-|\\.|\\/)\\d{2}\\1\\d{2}", "g");
  6. console.log( string.match(regex) );
  7. // => ["2017-06-27", "2017.06.27", "2017/06/27"]

修饰符

ES5 中修饰符,共 3 个:
修饰符 描述
g 全局匹配,即找到所有匹配的,单词是 global。
i 忽略字母大小写,单词是 ingoreCase。
m 多行匹配,只影响 ^ 和 $,二者变成行的概念,即行开头和行结尾。单词是 multiline。
当然正则对象也有相应的只读属性:

  1. var regex = /\w/img;
  2. console.log( regex.global );
  3. console.log( regex.ignoreCase );
  4. console.log( regex.multiline );
  5. // => true
  6. // => true
  7. // => true

source 属性

正则实例对象属性,除了 global、ingnoreCase、multiline、lastIndex 属性之外,还有一个 source 属性。
它什么时候有用呢?
比如,在构建动态的正则表达式时,可以通过查看该属性,来确认构建出的正则到底是什么:

  1. var className = "high";
  2. var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
  3. console.log( regex.source )
  4. // => (^|\s)high(\s|$) 即字符串"(^|\\s)high(\\s|$)"

构造函数属性

构造函数的静态属性基于所执行的最近一次正则操作而变化。除了是 $1,…,$9 之外,还有几个不太常用的 属性(有兼容性问题):
静态属性 描述 简写形式 RegExp.input 最近一次目标字符串 RegExp[“$_”] RegExp.lastMatch 最近一次匹配的文本 RegExp[“$&”] RegExp.lastParen 最近一次捕获的文本 RegExp[“$+”] RegExp.leftContext 目标字符串中lastMatch之前的文本 RegExp[“$`”] RegExp.rightContext 目标字符串中lastMatch之后的文本 RegExp[“$’”]
测试代码如下:

  1. var regex = /([abc])(\d)/g; var string = "a1b2c3d4e5"; string.match(regex);
  2. console.log( RegExp.input ); console.log( RegExp["$_"]); // => "a1b2c3d4e5"
  3. console.log( RegExp.lastMatch ); console.log( RegExp["$&"] ); // => "c3"
  4. console.log( RegExp.lastParen ); console.log( RegExp["$+"] ); // => "3"
  5. console.log( RegExp.leftContext ); console.log( RegExp["$`"] ); // => "a1b2"
  6. console.log( RegExp.rightContext ); console.log( RegExp["$'"] ); // => "d4e5"