别百度了,你要的正则表达式都在这里

562 阅读7分钟

前言

本文分享正则表达式的语法和常用方法,帮助大家理解羞涩难懂的正则表达式,自己动手,信手拈来。

文章末尾推荐一款好用的正则大全插件,提高大家开发效率。

RegExp构造函数

RegExp是js内置对象, 用以对字符串进行逻辑匹配运算。

创建正则表达式的三种方式

以下三种表达式都会创建相同的正则表达式:

  1. 字面量形式

    /ab+c/i;
    
  2. 首个参数为字符串模式的构造函数

    new RegExp('ab+c', 'i');
    
  3. 首个参数为常规字面量的构造函数

    new RegExp(/ab+c/, 'i');
    

原义文本字符

指字符串原本的含义

/abc/.test('abc123') // true

// 这里的abc就是原义文本字符
// 指检测字符串中是否有'abc'这个字符串, 而不是指 有a 或者 有b 或者 有c, 也不是指 有a 且 有b 且 有c

元字符

有特殊含义的字符, 区别于原义文本字符 (共11个):

[] {} () + . | $ ^ \ * ?

语法

字符类

  • /[abc]/ : 将a b c视为一类(具有相同特征), 所以, 只要字符串中 有a 或 有b 或有c 即为true
/[abc]/.test(/a123/); // true
/[abc]/.test(/ab123/); // true
/[abc]/.test(/123/); // false

负向类(反向类)

  • /[^abc]/: 将 不是a 或 不是b 或 不是c 视为一类, 所有, 只要字符串中有不是a b c其中任意一个的其他字符即为 true
/[^abc]/.test(/a123/); // true
/[^abc]/.test(/ab123/); // true
/[^abc]/.test(/123/); // true
/[^abc]/.test(/aaaa/); // false
/[^abc]/.test(/aabbcc/); // false

范围类

  • /[0-9]/: 0-9之间任意字符
  • /[a-z]/: a-z之间任意小写字母
  • /[A-Z]/: A-Z之间任意大写字母

注意点:

  1. 范围是一个闭区间;
  2. 可以连写: /[0-9a-zA-Z]/: 任何数字或字母都可以, 也可以分开/[0-46-9]/: 0-4 或 6-9;
  3. -表示范围, 右边一定要大于左边;
  4. [] 和 [^]的区别: 只要有字符串是xxx / 只要有字符串不是xxx;

预定义类

可直接使用的表达式

预定义类等价于含义
.[^\r\n]除回车和换行之外的所有字符
\d[0-9]数字字符
\D[^0-9]非数字字符
\s[\f\n\r\t\v]空白字符
\S[^\f\n\r\t\v]非空白字符
[\s\S]所有字符所有字符
\w[a-zA-Z_0-9]单词字符(字母、下划线、 数字)
\W[^a-zA-Z_0-9]非单词字符
/./.test(''); // false
/./.test(' '); // true 只要有东西就行
/./.test('\n'); // false 换行符不算内容

/\s/.test('哈哈'); // false
/\s/.test('哈\t哈'); // true
/\S/.test('哈哈'); // true
/\S/.test('哈\t哈'); // true

/\w/.test('aA_123'); // true
/\w/.test('aa+'); // true
/\w/.test('+-*'); // false
/\W/.test('aA_123'); // false
/\W/.test('aa+'); // true
/\W/.test('+-*'); // true

边界类

边界字符含义
^以XX开头
$以XX结尾
\b单词边界
\B非单词边界
  • /^abc/: 以a开头, 紧接bc
/^abc/.test('abc123'); // true
/^abc/.test('a1b2c3'); // false

# 注意: 不是以abc开头的意思!
  • /abc$/: 以c结尾, 前面紧接ab
/abc$/.test('123abc'); // true
/abc$/.test('1a2b3c'); // false

# 注意: 不是以abc结尾的意思!
  • /^abc$/: (开头)a + b + (结尾)c
/^abc$/.test('abc'); // true
/^abc$/.test('abcabc'); // false

# 注意: 不是以abc开头, 以abc结尾的意思!
  • /\b/

这里的单词边界 是指字符串中 独立的单词 而不是某一段字符中的一部分:

'This is a boy'.replace(/is/, 'ha') // Thha is a boy. 会默认替换第一个匹配的is
'This is a boy'.replace(/\bis/, 'ha') // This ha a boy. 只会匹配到单词is, 而不是This中的is
'This is a boy'.replace(/\Bis/, 'ha') // Thha is a boy. 匹配非单词的is, 和第一个效果一样

量词

量词含义
?出现0次或1次(最多出现一次),等价于 {0,1}
如果紧跟在任何量词 *、 +、? 或 {} 的后面,将会使量词变为非贪婪(匹配尽量少的字符),和缺省使用的贪婪模式(匹配尽可能多的字符)正好相反。例如,对 "123abc" 使用 /\d+/ 将会匹配 "123",而使用 /\d+?/ 则只会匹配到 "1"。
+出现一次或多次(至少出现一次)
*出现0次或多次(任意次)
{n}出现n
{n,m}出现n~m次
{n,}出现至少n次
.*具有贪婪的性质,首先匹配到不能匹配为止,根据后面的正则表达式,会进行回溯
.*?则相反,一个匹配以后,就往下进行,所以不会进行回溯,具有最小匹配的性质
// 有数字连续出现两次
/\d{2}/.test('a1b2'); // false
/\d{2}/.test('a12b3'); // true

// 有数字连续出现2~3次
/\d{2,3}/.test('a1b12c'); // true
/\d{2,3}/.test('a1b123c'); // true
/\d{2,3}/.test('a1b12345c'); // true 因为其中的123连续出现了3次

'123456789'.replace(/\d?/, 'a') // 'a23456789'
'123456789'.replace(/\d+/, 'a') // a
'123456789'.replace(/\d*/, 'a') // a
'x12345678'.replace(/\d*/, 'a') // ax12345678 会匹配0次,可以看成x的前面有个空'', 将''替换成a
'123456789'.replace(/\d{3}/, 'a') // a456789
'123456789'.replace(/\d{3,5}/, 'a') // a6789
'123456789'.replace(/\d{3,}/, 'a') // a

贪婪量词和惰性量词

贪婪和惰性是针对于搜索的工作原理而言的,在贪婪模式下,量词会尽可能多地重复,而当量词 *+?{} 的后面紧跟 ?时,将会使量词变为非贪婪,即匹配尽量少得字符。

比如,想匹配 'a "witch" and her "broom" is one' 中两个带引号的单词"witch""broom",你可能会这么写:

'a "witch" and her "broom" is one'.match(/".+"/g);

但是结果却是:"witch" and her "broom"

所以,你得这么写:

'a "witch" and her "broom" is one'.match(/".+?"/g);

才会得到想要的结果:['"witch"', '"broom"']

至于它是如何工作的,其中的原理是什么,感兴趣的同学可以参考我的这篇文章👉 今日说法 · 揭秘正则的惰性与贪婪,这里就不赘述了。

分组

() 捕获组

当模式的一部分用括号包起来 (...),那么这部分内容就可以称为“捕获组(capturing group)”,( ) 也称为“捕获括号”。

这有两个影响:

  1. 如果将量词放在 () 后面,则它会将括号视成一个整体。
  2. 它允许将匹配的一部分作为结果数组中的单独项;

比如,不带括号,/abc{3}/ 表示最后一个 c 重复3次。而 /(abc){3}/ 则会将 abc 视为整体, 重复3次。

/(abc){3}/.test('1abcabcabc'); // true
/(abc){3}/.test('a1bcabcabc'); // false
/abc{3}/.test('abcabcabc'); // false 不加()只能对就近的一个字符生效, 即匹配3个连续, 也就是匹配到abccc才算true

不仅如此,我们还能获取到 (...) 内匹配的内容,当使用 (...) 时, 正则会将 (...) 内匹配到的内容存放在 $ 中

$1-$9 属性是包含括号子串匹配的正则表达式的静态和只读属性。

// 取出()内的内容
const data = 'haha(123)hehe';
const _data = data.match(/\((.+)\)/);
RegExp.$1 // 123

我们一般用它来搭配 str.replace(),替换字符串中的内容,请看以下示例:

// 替换日期格式 2021-11-20 变成 20/11/2021
const date = '2021-11-20';
date.replace(/(\d{4})-(\d{2})-(\d{2})/, '$3/$2/$1'); // 20/11/2021
RegExp.$1 // 2021
RegExp.$2 // 11
RegExp.$3 // 20

如果我们仅仅需要括号,而不希望它们的内容出现在结果中,我们可以在开头添加 ?: 来排除捕获组。

例如,如果我们要查找 /(go)+/,但不希望括号内容(go)作为一个单独的数组项,则可以编写:/(?:go)+/

let str = "Gogogo John!";

// ?: 从捕获组中排除 'go'
let regexp = /(?:go)+ (\w+)/i;

let result = str.match(regexp);

alert( result[0] ); // Gogogo John(完整的匹配项)
alert( result[1] ); // John
alert( result.length ); // 2(在数组中没有其他数组项

另外,我们不仅可以在结果或替换字符串中使用捕获组 (...) 的内容,还可以在模式本身中使用它们,具体可以参考模式中的反向引用:\N 和 \k

| 选择(OR)

或运算, 会将 | 两边整体分开, 匹配左边或右边, 常搭配 ()元字符使用

  • /l(o|i)ve/: 匹配love或live
// 错误写法:
/lo|ive/.exec('love'); // lo, | 会将左右两边分开,会变成匹配lo或ive

// 正确写法
/l(o|i)ve/.exec('love'); // love

修饰符

影响当前整个正则规则的特殊符号, 共六个,可以连写多个,这里我们拿前三个说一说,更多修饰符请参考 修饰符

1. i (intensity)

不区分大小写

2. g (global)

全局匹配, 即全部匹配完,在使用时需要注意 lastIndex 的变化,请看我的踩坑总结:

  1. /g在匹配时, 由于多次匹配, 会在匹配到内容后, 记录下当前匹配内容的最后一个字符串的长度 lastIndex 。比如:

    const regex1 = new RegExp( 'foo', 'g' );
    const str1 = 'table football, foosball';
    
    regex1.test(str1); // true
    console.log(regex1.lastIndex); // 9
    

    此时 lastIndex 就是匹配到第一个 foo 时的长度,为 9。

  2. 当继续使用该正则进行匹配时, 会从上一次的lastIndex开始, 往后接着进行匹配

    regex1.test(str1); // true
    console.log(regex1.lastIndex); // 19
    

    可以看到, 再次使用时, 虽然结果同样为 true, 但 lastIndex 已为19, 说明匹配到了第二个 foo

  3. 当匹配不到时(匹配完了) 会返回 false, 并 重置 lastIndex 为0

    regex1.test(str1); // false
    console.log(regex1.lastIndex); // 0
    

    注意! 这里会发现匹配失败了, 说明此轮匹配已结束, lastIndex 并重置为 0 了, 如果再进行匹配, 又会重新开始, 所以会出现: true true false / true true false /true true false/....反复

这种现象还出现在 regexp.exec(str) 中,当 exec 使用了 g 修饰符后,会在重复调用改表达式时,一个接一个的返回所有匹配项,并更新 regexp.lastIndex 匹配项的位置,直到返回 null,并重置 lastIndex 为0。

过去,在将 str.matchAll 方法添加到 JavaScript 之前,会在循环中调用 regexp.exec 来获取组的所有匹配项

不过在 JavaScript 新增了 str.matchAll 后,我们就能更方便的获取所有匹配项了。

3. m (multiple)

检测换行符, 使用较少, 主要影响字符串的开始^与结束$边界

// m 需要和边界一起使用
const str = '我爱我的国家\n我爱我的国家\n我爱我的国家'

// 需求: 将每一行的第一个'我' 改成 '你'
str.replace(/我/, '你') // 只能替换第一个'我'
str.replace(/我/g, '你') // 会替换所有的'我'
str.replace(/^我/g, '你') // 同样只会替换第一个'我',
str.replace(/^我/gm, '你') // 成功, m会识别\n, 将\n后的第一个字符也识别成^的位置

其他特殊字符

字符含义
x(?=y)匹配'x'仅仅当'x'后面跟着'y'.这种叫做先行断言。例如,/Jack(?=Sprat)/会匹配到'Jack'仅当它后面跟着'Sprat'。/Jack(?=Sprat|Frost)/匹配‘Jack’仅当它后面跟着'Sprat'或者是‘Frost’。但是‘Sprat’和‘Frost’都不是匹配结果的一部分。
(?<=y)x匹配'x'仅当'x'前面是'y'.这种叫做后行断言。例如,/(?<=Jack)Sprat/会匹配到' Sprat '仅仅当它前面是' Jack '。/(?<=Jack|Tom)Sprat/匹配‘ Sprat ’仅仅当它前面是'Jack'或者是‘Tom’。但是‘Jack’和‘Tom’都不是匹配结果的一部分。
x(?!y)仅仅当'x'后面不跟着'y'时匹配'x',这被称为正向否定查找。例如,仅仅当这个数字后面没有跟小数点的时候,/\d+(?!.)/ 匹配一个数字。正则表达式/\d+|(?!.)/.exec("3.141")匹配‘141’而不是‘3.141’
(?<!y)x仅仅当'x'前面不是'y'时匹配'x',这被称为反向否定查找。例如, 仅仅当这个数字前面没有负号的时候,/(?<!-)\d+/ 匹配一个数字。 /(?<!-)\d+/.exec('3') 匹配到 "3". /(?<!-)\d+/.exec('-3') 因为这个数字前有负号,所以没有匹配到。

正则的常用方法

方法描述
exec一个在字符串中执行查找匹配的RegExp方法,它返回一个数组(未匹配到则返回 null)。
test一个在字符串中测试是否匹配的RegExp方法,它返回 true 或 false。
match一个在字符串中执行查找匹配的String方法,它返回一个数组,在未匹配到时会返回 null。
matchAll一个在字符串中执行查找所有匹配的String方法,它返回一个迭代器(iterator)。
search一个在字符串中测试匹配的String方法,它返回匹配到的位置索引,或者在失败时返回-1。
replace一个在字符串中执行查找匹配的String方法,并且使用替换字符串替换掉匹配到的子字符串。
split一个使用正则表达式或者一个固定字符串分隔一个字符串,并将分隔后的子字符串存储到数组中的 String 方法。

注意: 使用顺序不要搞反:

RegExp方法指: 正则表达式的方式, 例如: /正则/.exec('字符串')

String方法指: 字符串的方法, 参数可以是正则, 例如: '字符串'.match(/正则/)

String.prototype.match()

当 match 不带有修饰符 g,则以数组的形式返回匹配到的第一个项。除此之外,还包含捕获组、属性(index)、源字符串(input)。

let str = "I love JavaScript";

let result = str.match(/Java(Script)/);

alert( result[0] );     // JavaScript(完全匹配的结果)
alert( result[1] );     // Script(第一个捕获组)
alert( result.length ); // 2

// 其他信息:
alert( result.index );  // 7(匹配位置)
alert( result.input );  // I love JavaScript(源字符串)

如果带有 g,则它将只返回一个包含所有匹配项的数组,但不包含捕获组和其它详细信息。

let str = "I love JavaScript";

let result = str.match(/Java(Script)/g);

alert( result ); // ['JavaScript'],没有捕获组)
alert( result.length ); // 1

String.prototype.matchAll()

matchAllmatch 的“更新、改进”的变体。因为 match 在搜索所有组的所有匹配项时,只返回了匹配数组,而没有每个匹配项的详细信息。

所以 matchAll 相当于将每个匹配项的完整信息以数组的方式保留,然后以可迭代对象的形式返回。你可以理解为,一个 matchAll 将许许多多的小 match 数组塞进可迭代对象中返回。

如果我们想取出匹配项,可以先使用 Array.from 或结构将其转成数组。当然,如果只是遍历结果,那使用 for...of 就足够了。

eg: 想从'game_loop1.2.1'这段字符串中提取出'gameloop'部分

const str = 'game_loop1.2.1';

// 1. matchAll返回的是一个迭代器
const iterator = str.matchAll(/[a-zA-Z]*/g)
console.log(iterator, Object.prototype.toString.call(iterator))
// expected output:
// RegExpStringIterator {}[[Prototype]]: RegExp String Iterator
// '[object RegExp String Iterator]'

// 2. 将迭代器获获到的所有匹配正则表达式的结果及分组捕获组放入数组:
const arr = [...iterator];

// 3. 拼接
const gameloop = arr.reduce((prev, item) => {
    prev.push(item[0]);
    return prev;
}, []).join('');

console.log(gameloop)

arr数组:

image.png

正则大全-插件

家人们还在自己找正则?别浪费时间了,瞧这一天天不够你闲的......赶紧上车 👉any-rule

你想要的正则都在这里 👇

正则大全

参考资料