Javascript 正则表达式
- 在线编写和检验正则的网站:https://regex101.com 支持多语言切换
- freeCodeCamp: Introduction to the Regular Expression Challenges
基础概念
创建方式
构造函数
const regex = new RegExp('abc', 'i');
字面量
const regex = /abc/i;
修饰符
g:global,全文搜索,默认搜索到第一个结果停止搜索。i:insensitive,不区分大小写,默认大小写敏感。m:multiple line,多行搜索,更改^和$的含义,使它们分别在每行的行首和行尾匹配,默认是在整个字符串的开头和结尾匹配。s:single line,单行模式,此模式下.能匹配任意字符,包括换行符,默认是不包括的。
TODO
y:sticky,必须从上一次匹配成功的下一个位置开始(lastIndex),作用是达到^在全局匹配中都有效。u:unicode,用来正确处理大于\uFFFF的 Unicode 字符(对于大于\uFFFF的字,把 4 字节的字符当做单独 1 个字来解析,否则当做 2 个 2 字节的字),把\u{}当做 Unicode(否则当做正则内容)。默认按照 ES5 来处理字符,1 个字仅有 2 个字节。
元字符
大部分字符在正则表达式中,就是字面的含义,比如/a/匹配 a,/b/匹配 b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的 a 和 b),那么它们就叫做“字面量字符”(literal characters)。
除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(meta characters),先介绍以下几个:
| 元字符 | 符号 | 说明 |
|---|---|---|
| 选择类 | | | 或匹配,如 /x|y/ 正则可匹配 x 或 y 两个字符 |
| 选择类 | [] | 或匹配,如 [abcd] 代表一个字符,这个字符可以是 abcd 四个字符中的任意 一个 |
| 范围类 | [0-9] | 0-9 中的任意一个字符 |
| 范围类 | [a-z] | a-z 中的任意一个字符 |
| 范围类 | [a-zA-Z0-9] | 大写字母、小写字母、数字中的任意一个字符 |
| 位置类 | ^ | 匹配字符串的开始 |
| 位置类 | $ | 匹配字符串的结束 |
| 预定义 | . | 匹配除回车(\r)、换行(\n) 、行分隔符(\u2028)和段分隔符(\u2029)以外的任意字符 |
| 预定义 | \w | 匹配字母数字下划线,等同于 [a-zA-Z0-9_] |
| 预定义 | \s | 匹配任意空白符,等同于 [\r\n\t\f\v ] |
| 预定义 | \d | 匹配数字,等同于 [0-9] |
| 预定义 | \b | 匹配单词(字母、数字、下划线)边界,注意:- 也被认为是单词边界 |
| 预定义 | [\b] | 匹配退格符(backspace) |
| 预定义 | \t | 匹配水平制表符(tab) |
| 预定义 | \r | 匹配回车符(carriage return) |
| 预定义 | \n | 匹配换行符(linefeed) |
| 预定义 | \0 | 匹配空字符(NUL)不要在此后面跟小数点 |
| 预定义 | \v | 匹配垂直制表符(vertical tab) |
| 预定义 | \f | 匹配换页符(form-feed) |
其它元字符将会在后面进行介绍。
转义
需要使用 \ 进行转义的元字符:
| 字符 | 说明 |
|---|---|
| \ | 因为已在转义中使用 |
| ( | 因为已在捕获中使用 |
| ) | 因为已在捕获中使用 |
| [ | 因为已在范围类和反义中使用 |
| . | 因为已在预定义字符中使用 |
| ^ | 因为已在位置类字符中使用 |
| $ | 因为已在位置类字符中使用 |
| | | 因为已在选择类字符中使用 |
| * | 因为已在量词类中使用 |
| + | 因为已在量词类中使用 |
| ? | 因为已在量词类中使用 |
反义
| 反义字符 | 说明 |
|---|---|
| [^x] | 匹配除“x”之外的所有字符,其中“x”可以为任意字符 |
| [^xyz] | 同上,匹配除“x、y、z”之外的任意字符 |
| \W | 匹配除了字母、数字、下划线之外的所有字符,等同于 [^\w] |
| \S | 匹配除空白符之外的任意字符,等同于 [^\s] |
| \B | 匹配不是单词边界的字符,等同于 [^\b] |
| \D | 匹配不是数字的所有字符,等同于 [^\d] |
贪婪模式重复匹配
贪婪模式:即匹配直到下一个字符不满足匹配规 则为止,这是一种最大可能匹配。
| 元字符 | 符号 | 重复出现次数 |
|---|---|---|
| 量词符 | * | ≥ 0 |
| 量词符 | + | ≥ 1 |
| 量词符 | ? | 0 或 1 |
| 精确匹配 | {n} | n |
| 精确匹配 | {n,} | ≥ n |
| 精确匹配 | {m,n} | m ≤ count ≤ n |
惰性模式重复匹配
惰性模式:即一旦条件满足,就不再往下匹配,这是一种最小可能匹配。
| 元字符 | 符号 | 重复出现次数,但尽可能少的重复 |
|---|---|---|
| 量词符 | *? | ≥ 0 |
| 量词符 | +? | ≥ 1 |
| 量词符 | ?? | 0 或 1 |
| 精确匹配 | {n}? | n |
| 精确匹配 | {n,}? | ≥ n |
| 精确匹配 | {m,n}? | m ≤ count ≤ n |
常用正则
- http(s) url:
/^(https?:)?\/\/.+/ - Phone:
/^(0|86|17951)?(13[0-9]|15[012356789]|166|17[3678]|18[0-9]|14[57])[0-9]{8}$/ - Email:
/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/ - National ID:
/^(^[1-9]\d{7}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])\d{3}$)|(^[1-9]\d{5}[1-9]\d{3}((0\d)|(1[0-2]))(([0|1|2]\d)|3[0-1])((\d{4})|\d{3}[Xx])$)$/ - Post Code:
/^[1-9]\d{5}(?!\d)$/
更多参考: https://www.baidufe.com/fehelper/regexp/index.html
RegExp 实例
属性
.ignoreCase:返回一个布尔值,是否大小写敏感,默认是false。.global: 是否全局搜索,默认是false。.multiline: 多行搜索,默认值是false。.lastIndex: 下一次开始搜索的位置,每次正则表达式成功匹配时,lastIndex属性值都会随之改变,可读写,只要手动设置了lastIndex的值,就会从指定位置开始匹配。.source: 返回正则表达式的字符串形式(不包括反斜杠),该属性只读。
RegExp.prototype.test(str)
测试字符串参数中是否存正则表达式模式,如果存在则返回true,否则返回false。
const r = /x/g;
const s = '_x_x';
r.lastIndex; // 0
r.test(s); // true
如果 正则表达式带有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配。
r.lastIndex; // 2
r.test(s); // true
如果正则模式是一个空字符串,则匹配所有字符串。
new RegExp('').test('abc'); // true
RegExp.prototype.exec(str)
该方法用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回 null。
const s = '_x_x';
/x/.exec(s) // ["x"]
/y/.exec(s) // null
如果表达式中有正则捕获,则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组:
/_(x)/.exec('_x_x'); // ["_x", "x"]
exec 方法的返回数组还包含以下两个属性:
input:整个原字符串。index:整个模式匹配成功的开始位置(从0开始计数)。
如果正则表达式加上 g 修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
利用g修饰符允许多次匹配的特点,可以用一个循环遍历到所有匹配的字符:
const reg = /a/g;
const str = 'abc_abc_abc';
while (true) {
const match = reg.exec(str);
if (!match) break;
console.log(match[0]);
}
// a
// a
// a
以上代码每轮循环也是基于不断更新 lastIndex 属性工作的,lastIndex 只对同一个正则表达式有效,所以下面这样写是错误的,会造成死循环:
while (true) {
const match = /a/.exec('abc_abc_abc');
if (!match) break;
console.log(match[0]); // 0 代表整个匹配的,如果有分组则从 1 开始
}
因为 while 循环的每次匹配条件都是一个新的正则表达式,导致 lastIndex 属性总是等于 0。
String 实例方法(正则相关)
String.prototype.match(regex)
'_x_x'.match(/x/); // ["x"]
'_x_x'.match(/y/); // null
字符串的match方法与正则对象的exec方法非常类似,但正则表达 式带有g修饰符,则该方法与正则对象的exec方法返回值不同,会一次性返回所有匹配成功的结果:
'_x_x'.match(/x/g); // ["x", "x"]
/x/g.exec('_x_x'); // ["x"]
String.prototype.search(regex)
返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1:
'_x_x'.search(/x/); // 1
'_x_x'.search(/y/); // -1
String.prototype.replace(regex, str | func)
正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值。
'_x_x'.replace(/x/, 'y'); // _y_x
'_x_x'.replace(/x/g, 'y'); // _y_y
第二个参数是字符串时,其中可以使用美元符号$,用来指代所替换的内容。
$&:匹配的字符串。- $`:匹配的字符串前面的文本。
$':匹配字符串后面的文本。$n:匹配成功的第 n 组内容,n 是从 1 开始的自然数。$$:指代美元符号$。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1'); // "world hello"
'abc'.replace('b', "[$`-$&-$']"); // "a[a-b-c]c"
replace 方法也可以用来消除字符串首尾两端的空格,实现 trim 函数的效果,也可以根据不同场景变得更加灵活:
var str = ' #id div.class ';
str.replace(/^\s+|\s+$/g, ''); // "#id div.class"
第二个参数既可以是字符串也可以是函数:
'_x_x'.replace(/x/, 'y'); // _y_x
'_x_x'.replace(/x/, function (match) {
return match.toUpperCase();
}); // _X_x
其中函数第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数,通常用$number表示),此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串:
'_x_y_z'.replace(/(x).*(y).*(z).*/g, function (match, $1, $2, $3, index, str) {
console.log(match, $1, $2, $3); // x_y_z x y z
console.log(index, str); // 1 _x_y_z
return $1 + $2 + $3; // _xyz
});
如果要全局遍历匹配的字符串,除了用上文提到的 while + exec 方式,还可以用 replace 方法,不要返回任何值,虽然最后会返回undefined,但并不会更改原字符串:
const str = 'abc_abc_abc';
str.replace(/a/g, function (match) {
console.log(match);
});
// a
// a
// a
str; // abc_abc_abc
String.prototype.split(regex[, num])
这里主要介绍该方法在正则中的应用。
// 非正则分隔
'a, b,c, d'.split(','); // [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a, b,c, d'.split(/,\s*/); // [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
'a, b,c, d'.split(/,\s*/, 2); // [ 'a', 'b' ]
捕获
正则表达式中用 () 来表示分组,例如:/([0-9])/,() 会把 每个分组里匹配的值保存起来。
捕获型 ()
捕获与引用
被正则表达式匹配到的字符串会被暂存起来。
分组捕获的串从 1 开始编号,$1 表示第一个被捕获的串, $2 是第二个,以此类推,我们可以通过 $1,$2... 引用这些串。
let reg = /(\d{4})-(\d{2})-(\d{2})/;
let data = '2017-10-24';
reg.test(data);
RegExp.$1; //2017
RegExp.$2; //10
RegExp.$3; //24
与 replace 配合
String.prototype.replace 方法的传参中可以直接引用被捕获的串。比如我们想将日期 10.24/2017 改为 2017-10-24 :
let reg = /(\d{2})\.(\d{2})\/(\d{4})/;
let data = '10.24/2017';
data = data.replace(reg, '$3-$1-$2');
console.log(data); //2017-10-24
给 replace 传递迭代函数可以优雅地解决一些问题:
将违禁词转换为等字数的星号是一个常见的需求,比如文本是 dot is a doubi ,其中 dot 和 doubi 是违禁词,转换后应为 *** is a ***** 。
let reg = /(dot|doubi)/g;
let str = 'dot is a doubi';
str = str.replace(reg, function (word) {
return word.replace(/./g, '*');
});
console.log(str); //*** is a *****
replace 与正则捕获组匹配还有一个常见用法,将浮点数左边的数从右向左每三位添加一个逗号,匹配全局中,数字后面跟随的是(以 . 结尾的、三个数字的分组至少有一组)的串。
function commafy(num) {
return num.toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($2) {
return $2 + ',';
});
}
console.log(commafy(1200000000.11)); //1,200,000,000.11
console.log(commafy(123246723749.213769283)); //123,246,723,749.21378
嵌套分组的捕获
如果碰到类似 /((dot) is (a (doubi)))/ 这种嵌套分组,规则是以左括号出现的顺序进行捕获。
let reg = /((dot) is (a (doubi)))/;
let str = 'dot is a doubi';
reg.test(str); //true
console.log(RegExp.$1); //dot is a doubi
console.log(RegExp.$2); //dot
console.log(RegExp.$3); //a doubi
console.log(RegExp.$4); //doubi
反向引用
let reg = /(\w{3}) is \1/;
console.log(reg.test('dot is dot')); //true
console.log(reg.test('dolby is dolby')); //false
console.log(reg.test('dot is tod')); //false
console.log(reg.test('dolby is dlboy')); //false
\1 引用了第一个被分组所捕获的串,本例中即 (\w{3}) ,表达式是动态决定的,如果编号越界了会被当成普通的表达式。
let reg = /(\w{3}) is \3/;
console.log(reg.test('dot is \3')); //true
console.log(reg.test('dolby is dolby')); //false
尝试用反向引用解决外观数列问题。
非捕获型 (?: )
有时我们只是想分个组,并没有捕获的需求,这种情况下可以使用非捕获性分组,语法为 (?:) 。
let reg = /(?:\d{4})-(\d{2})-(\d{2})/;
let date = '2017-10-24';
console.log(reg.test(date)); //true
console.log(RegExp.$1); //10
console.log(RegExp.$2); //24
这个例子中,(?:\d{4}) 分组不会捕获任何串,所以 $1 为 (\d{2}) 捕获的串。
断言 Assertion
断言虽然包裹在 () ,但并不会捕获值。
以下所说的前和后指的要匹配的内容相对于断言的前后位置。
正向前瞻型 (?= )
Lookahead assertion x(?=y): Matches "x" only if "x" is followed by "y". For example:
let reg = /dot is a (?=doubi)/;
console.log(reg.test('dot is a doubi')); //true
console.log(reg.test('dot is a shadou')); //false
这个正则要求 dot is a 后面要是 doubi 才匹配成功。
反向前瞻型 (?! )
Negative lookahead assertion x(?!y): Matches "x" only if "x" is not followed by "y". For example:
let reg = /dot is a (?!doubi)/;
console.log(reg.test('dot is a doubi')); //false
console.log(reg.test('dot is a shadou')); //true
这个正则要求 dot is a 后面除了 doubi ,都能匹配成功。
正向后瞻型 (?<= )
Lookbehind assertion (?<=y)x: Matches "x" only if "x" is preceded by "y". For example:
let reg = /(?<=dot) is a doubi/;
console.log(reg.test('dot is a doubi')); //true
console.log(reg.test('pot is a doubi')); //false
这个正则要求 is a doubi 前面要是 dot 才匹配成功。
反向后瞻型 (?<! )
Negative lookbehind assertion (?<!y)x: Matches "x" only if "x" is not preceded by "y". For example:
let reg = /(?<!dot) is a doubi/;
console.log(reg.test('dot is a doubi')); //false
console.log(reg.test('pot is a doubi')); //true
这个正则要求 is a doubi 前面除了 dot ,都能匹配成功。
前瞻型分组与非捕获型都不会捕获值,那么它们的区别是什么?
A: 非捕获型分组匹配到的串仍会被外层的捕获型分组捕获到,但前瞻型却不会,当你需要参考后面的值,又不想连它一起捕获时,前瞻型分组就派上用场了:
let str = 'dot is a doubi';
let reg;
相同点:
reg = /dot is a (?:doubi)/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //无结果
reg = /dot is a (?=doubi)/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //无结果
不同点:
reg = /(dot is a (?:doubi))/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //dot is a doubi
reg = /(dot is a (?=doubi))/;
console.log(reg.test(str)); //true
console.log(RegExp.$1); //dot is a