# 正则表达式

正则表达式是表达文本模式的方法。

有三种生成正则表达式的方式, 可以使用字面量、构造函数和工厂方法来创建正则表达式

/pattern/flags
new RegExp(pattern[, flags])
RegExp(pattern[, flags])

# 1、RegExp构造函数

构造函数的使用和参数, 参数pattern可以是字面量或者字符串

  • 文字符号的 参数用斜杠括起来,不使用引号。
  • 构造函数的 参数不包含在斜杠之间,但使用引号。
// 字面量正则表达式 /ab+c/i 和下面的构造方法创建的是一样的
new RegExp(/ab+c/, 'i') // 字面量
new RegExp('ab+c', 'i') // 构造函数

当表达式被求值时,文字表示法会导致对正则表达式的编译。当正则表达式保持不变时,请使用字面量表示法。例如,如果使用字面量表示法来构造循环中使用的正则表达式,则不会在每次迭代时重新编译正则表达式。

# 1.1 实例属性

  1. ignoreCase 是否设置了i属性, 忽略大小学

  2. global 是否设置了g属性,全局匹配

    如果不带g,正则过程中字符串从左到右匹配,找到第一个符合条件的即匹配成功,返回 如果带g,则字符串从左到右,找到每个符合条件的都记录下来,直到字符串结尾位置

  3. multiline 是否设置了m属性,多行匹配

    若存在换行\n并且有开始^或结束$符的情况下,和g一起使用实现全局匹配,因为存在换行时默认会把换行符作为一个字符任务匹配字符串是个单行,g只匹配第一行,添加m之后实现多行,每个换行符之后就是开始

  4. lastIndex 返回一个数值,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义.

  5. source 返回正则表达式的字符串形式(不包括反斜杠),该属性只读。

const reg = /abc/gim;

console.log(reg.ignoreCase); // true
console.log(reg.global); // true
console.log(reg.multiline); // true

console.log(reg.source); // abc

# 1.2 实例方法

# 1.2.1 RegExp.prototype.test()

正则实例对象的test方法返回一个布尔值,表示当前模式是否能匹配参数字符串。

const reg1 = /cat/;
console.log(reg1.test("a white cat")); // true

如果正则包含g属性, 则每一次test都从上一次结束的位置开始向后匹配

const reg2 = /x/g;
const str = '_x_x';

console.log(reg2.lastIndex); // 0
console.log(reg2.test(str)); // true
console.log(reg2.lastIndex); // 2
console.log(reg2.test(str)); // true
console.log(reg2.lastIndex); // 4
console.log(reg2.test(str)); // false

带有g修饰符时,可以通过正则对象的lastIndex属性指定开始搜索的位置。

const reg2 = /x/g;
const str = '_x_x';
reg2.lastIndex = 4; // 从第五个位置开始搜索, 不会匹配返回false
console.log(reg2.test(str)); // false

lastIndex属性只对同一个正则表达式有效,所以下面这样写是错误的。所以下面这种情况会导致死循环

let count = 0;
while (/a/g.test('babaa')) count++;

如果正则模式是一个空字符串,则匹配所有字符串。

new RegExp('').test('abc') // true

# 1.2.2 RegExp.prototype.exec()

exec方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null

const reg = /\d+/;
const str = '_12_34';
console.log(reg.exec(str)); // [ '12', index: 1, input: '_12_34', groups: undefined ]

注意返回的结果是一个数组, 数组的长度是1, 但是这个数组也包含了index和input等属性

如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。

const reg = /_(\d+)/;
const str = '_12_34';
console.log(reg.exec(str)); // [ '_12', '12', index: 0, input: '_12_34', groups: undefined ]

如果正则表达式加上g修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始。

const reg = /_(\d+)/g;
const str = '_12_34';
console.log(reg.exec(str)); // [ '_12', '12', index: 0, input: '_12_34', groups: undefined ]
console.log(reg.exec(str)); // [ '_34', '34', index: 0, input: '_12_34', groups: undefined ]

利用g修饰符允许多次匹配的特点,可以用一个循环完成全部匹配。获取到字符串里面所有符合条件的

const reg = /\d+/g;
const str = '123_456-789';
while (true) {
  const result = reg.exec(str);
  if (!result) {
    break;
  }
  console.log(`#${result.index}:${result[0]}`);
  /* 
    #0:123
    #4:456
    #8:789
  */
}

正则实例对象的lastIndex属性不仅可读,还可写。设置了g修饰符的时候,只要手动设置了lastIndex的值,就会从指定位置开始匹配。

# 2、字符串的实例方法

# 2.1 String.prototype.match()

字符串的match方法与正则对象的exec方法非常类似:匹配成功返回一个数组,匹配失败返回null

const str = 'abc';
const ret = str.match(/b/);
console.log(ret); // [ 'b' ]
// 当然这个ret还包含index、input等属性

如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果。

const str = '12ab34cd56';
const ret = str.match(/\d+/g);
console.log(ret); // [ '12', '34', '56' ]	获取到了所有的数字结果

设置正则表达式的lastIndex属性,对match方法无效,匹配总是从字符串的第一个字符开始。

字符串对象的search方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1

# 2.3 String.prototype.replace()

查看replace

# 2.4 String.prototype.split()

字符串对象的split方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。

该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数

const str = '1,  2, 3,4,   5';
const ret = str.split(/,\s*/); // [ '1', '2', '3', '4', '5' ]
const ret2 = str.split(','); // [ '1', '  2', ' 3', '4', '   5' ]
const ret3 = str.split(/,\s*/, 2); // [ '1', '2' ]

如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回。

const str = '1aa2a3aaa4';
const ret1 = str.split(/a*/); // [ '1', '2', '3', '4' ]
const ret2 = str.split(/(a*)/); // [ '1', 'aa', '2', 'a', '3',  'aaa', '4' ]

# 3、匹配规则

# 3.1 字面量字符

大部分字符在正则表达式中,就是字面的含义,比如/a/匹配a/b/匹配b。如果在正则表达式之中,某个字符只表示它字面的含义(就像前面的ab),那么它们就叫做“字面量字符”(literal characters)。

除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个。

# 3.2 元字符

  1. 点字符.

    点字符(.)匹配除回车(\r)、换行(\n) 、行分隔符(\u2028)和段分隔符(\u2029)以外的所有字符。注意,对于码点大于0xFFFF的 Unicode 字符,点字符不能正确匹配,会认为这是两个字符。

    /c.t/
    

    上面代码中,c.t匹配ct之间包含任意一个字符的情况,只要这三个字符在同一行,比如catc2tc-t等等,但是不匹配coot

  2. 位置字符

    位置字符

    ^ 表示字符串的开始位置

    $ 表示字符串的结束位置

    // test必须出现在开始位置
    /^test/.test('test123') // true
    
    // test必须出现在结束位置
    /test$/.test('new test') // true
    
    // 从开始位置到结束位置只有test
    /^test$/.test('test') // true
    /^test$/.test('test test') // false
    
  3. 选择符 |

    竖线符号(|)在正则表达式中表示“或关系”(OR),即cat|dog表示匹配catdog

  4. 其他的元字符还包括\\\*+?()[]{}等,将在下文解释。这些其实都还好理解,比较基础的。

# 3.3 转义符

正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成\+

const ret = /1+1/.test('1+1'); // false
const ret2 = /1\+1/.test('1+1'); // true

正则表达式中,需要反斜杠转义的,一共有12个字符:^.[$()|*+?{\。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。

(new RegExp('1\+1')).test('1+1')
// false

(new RegExp('1\\+1')).test('1+1')
// true

上面代码中,RegExp作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义。

# 3.4 特殊字符

正则表达式对一些不能打印的特殊字符,提供了表达方法。

  • \cX 表示Ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符。
  • [\b] 匹配退格键(U+0008),不要与\b混淆。
  • \n 匹配换行键。
  • \r 匹配回车键。
  • \t 匹配制表符 tab(U+0009)。
  • \v 匹配垂直制表符(U+000B)。
  • \f 匹配换页符(U+000C)。
  • \0 匹配null字符(U+0000)。
  • \xhh 匹配一个以两位十六进制数(\x00-\xFF)表示的字符。
  • \uhhhh 匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符。

# 3.5 字符类 [] 中括号

字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示xyz之中任选一个匹配。 一般会用中括号表示字符类

/[abc]/.test('hello world') // false
/[abc]/.test('apple') // true

上面代码中,字符串hello world不包含abc这三个字母中的任一个,所以返回false;字符串apple包含字母a,所以返回true

有两个字符在字符类中有特殊含义。

# (1)脱字符(^)

如果方括号内的第一个字符是[^],则表示除了字符类之中的字符,其他字符都可以匹配。比如,[^xyz]表示除了xyz之外都可以匹配。

/[^abc]/.test('hello world') // true
/[^abc]/.test('bbc') // false

上面代码中,字符串hello world不包含字母abc中的任一个,所以返回true;字符串bbc不包含abc以外的字母,所以返回false

如果方括号内没有其他字符,即只有[^],就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.)是不包括换行符的。

注意

脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。

# (2)连字符(-)

某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c][0123456789]可以写成[0-9],同理[A-Z]表示26个大写字母。下面都是一些比较合法的写法

[0-9.,]
[0-9a-fA-F]
[a-zA-Z0-9-]
[1-31] // [1-31],不代表1到31,只代表1到3。

连字符还可以用来指定 Unicode 字符的范围。比如 \u0128-\uFFFF表示匹配码点在0128FFFF之间的所有字符。

var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str); // true

举个例子,下面就是匹配只由中文汉字组成的字符串

const reg = /^[\u4e00-\u9fa5]+$/;
const str = '你好世界';
console.log(reg.test(str)); // true

TIP

如果连字符字符类中的第一个或最后一个,则不需要对其进行转义

# 3.6 预定义模式

预定义模式指的是某些常见模式的简写方式。

  • \d 匹配0-9之间的任一数字,相当于[0-9]
  • \D 匹配所有0-9以外的字符,相当于[^0-9]
  • \w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]
  • \W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]
  • \s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]
  • \S 匹配非空格的字符,相当于[^ \t\r\n\v\f]
  • \b 匹配词的边界。
  • \B 匹配非词边界,即在词的内部。

# 3.7 重复类

模式的精确匹配次数,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次。

/lo{2}/.test('look'); // true 表示连续出现2次o
/lo{2,5}k/.test('looook') // true 指定o连续出现2次到5次之间

# 3.8 量词符

量词符用来设定某个模式出现的次数。

  • ? 问号表示某个模式出现0次或1次,等同于{0, 1}
  • * 星号表示某个模式出现0次或多次,等同于{0,}
  • + 加号表示某个模式出现1次或多次,等同于{1,}

# 3.9 贪婪模式

上一小节的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。

const reg = /a+/;
const s = 'baaab';
console.log(s.match(reg)); // [ 'aaa']

上面的代码中, 模式是/a+/, 表示匹配一个a或者多个a, 那么到底匹配几个a呢?因为默认是贪婪模式,会一直匹配到字符a不再出现为止, 所以匹配结果是3个a。

提示

如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号。

const reg = /a+?/;
const s = 'baaab';
console.log(s.match(reg)); // [ 'a']

上面代码中,模式结尾添加了一个问号/a+?/,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。

除了非贪婪模式的加号,还有非贪婪模式的星号(*)。

  • *?:表示某个模式出现0次或多次,匹配时采用非贪婪模式。
  • +?:表示某个模式出现1次或多次,匹配时采用非贪婪模式。

WARNING

.*? 是什么意思? 我的理解是匹配任意字符的非贪婪模式,具体可以查看下面的demo

const reg1 = /a(.*?)bc/;
const reg2 = /a(.*)bc/;
const s = 'abcabc';
console.log(reg1.exec(s)[1]); // ''
console.log(reg2.exec(s)[1]); // 'bca'

# 3.10 修饰符

修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。

修饰符可以单个使用,也可以多个一起使用。

// 单个修饰符
var regex = /test/i;

// 多个修饰符
var regex = /test/ig;
  • (1) g 修饰符

    默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。

    var regex = /b/;
    var str = 'abba';
    
    regex.test(str); // true
    regex.test(str); // true
    regex.test(str); // true
    

    上面代码中,正则模式不含g修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回true

    var regex = /b/g;
    var str = 'abba';
    
    regex.test(str); // true
    regex.test(str); // true
    regex.test(str); // false
    

    上面代码中,正则模式含有g修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba只有两个b,所以前两次匹配结果为true,第三次匹配结果为false

  • (2) i 修饰符

    默认情况下,正则对象区分字母的大小写,加上i修饰符以后表示忽略大小写(ignorecase)。

    /abc/.test('ABC') // false
    /abc/i.test('ABC') // true
    

    上面代码表示,加了i修饰符以后,不考虑大小写,所以模式abc匹配字符串ABC

  • (3) m修饰符

    m修饰符表示多行模式(multiline),会修改^$的行为。默认情况下(即不加m修饰符时),^$匹配字符串的开始处和结尾处,加上m修饰符以后,^$还会匹配行首和行尾,即^$会识别换行符(\n)。

    /world$/.test('hello world\n') // false
    /world$/m.test('hello world\n') // true	
    

    上面的代码中,字符串结尾处有一个换行符。如果不加m修饰符,匹配不成功,因为字符串的结尾不是world;加上以后,$可以匹配行尾。

    /^b/m.test('a\nb') // true
    

    上面代码要求匹配行首的b,如果不加m修饰符,就相当于b只能处在字符串的开始处。加上b修饰符以后,换行符\n也会被认为是一行的开始。

# 4、组匹配

# (1)概述

正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。

/fred+/.test('fredd') // true
/(fred)+/.test('fredfred') // true

上面代码中,第一个模式没有括号,结果+只表示重复字母d,第二个模式有括号,结果+就表示匹配fred这个词。

const reg = /(.)b(.)/;
const str = 'abcabc';
console.log(str.match(reg)); // [ 'abc', 'a', 'c' ]

上面代码中,正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c

WARNING

注意,使用组匹配时,不宜同时使用g修饰符,否则match方法不会捕获分组的内容。

const reg = /(.)b(.)/g;
const str = 'abcabc';
console.log(str.match(reg)); // [ 'abc', 'abc' ]

上面代码使用带g修饰符的正则表达式,结果match方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec方法,配合循环,才能读到每一轮匹配的组捕获。

const reg = /(.)b(.)/g;
const str = 'abcabc';
// console.log(str.match(reg)); // [ 'abc', 'abc' ]
while (true) {
  const ret = reg.exec(str);
  if (!ret) break;
  console.log(ret);
}
/* 
[ 'abc', 'a', 'c' ]
[ 'abc', 'a', 'c' ]
*/

正则表达式内部,还可以用\n引用括号匹配的内容,n是从1开始的自然数,表示对应顺序的括号。

const ret = /(.)b(.)\1b\2/.test('abcabc'); // true

上面的代码中,\1表示第一个括号匹配的内容(即a),\2表示第二个括号匹配的内容(即c)。

下面是另外一个例子。

/y(..)(.)\2\1/.test('yabccab') // true

还有括号还可以嵌套的等等

组匹配非常有用,下面是一个匹配网页标签的例子。

var tagName = /<([^>]+)>[^<]*<\/\1>/; // 需要了解一下脱字符^
const tag = tagName.exec('<div>hello</div>')[1]; // div

稍加修改, 变成可以获取标签属性的正则

const reg = /<(\w+)([^>]*)>(.*?)<\/\1>/g;
const tag = '<b class="bold">bbbb</b><i>iiii</i>';
while (true) {
  const ret = reg.exec(tag);
  if (!ret) break;
  console.log([ret[1], ret[2], ret[3]]);
  /* 
  [ 'b', ' class="bold"', 'bbbb' ]
  [ 'i', '', 'iiii' ]
  */
}
# (2)非捕获组

(?:x)称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。

非捕获组的作用请考虑这样一个场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容。

看一个例子

const reg = /(.)b(.)/;
const str = 'abc';
console.log(reg.exec(str)); // [ 'abc', 'a', 'c']

const reg = /(?:.)b(.)/;
const str = 'abc';
console.log(reg.exec(str)); // [ 'abc', 'c']
/*
上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容。
*/

再看一个分解网址的正则

const urlReg = /(?:https?|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)/;
const url = 'https://google.com/search?q=reg';
console.log(urlReg.exec(url));
// [ 'https://google.com/search?q=reg', 'google.com', '/search?q=reg' ]

协议的捕获组是非捕获组, 不会在结果中返回

# (3)先行断言

x(?=y)称为先行断言(Positive look-ahead), x只有在y前面才匹配,y不会被记入返回结果。 比如要匹配后面跟着百分号的数字, 可以这么写/\d+(?=%)/

先行断言中括号的内容是不会被返回的

const reg = /\d+(?=%)/g
const str = '100%------76%'
console.log(reg.exec(str));// ['100']
console.log(reg.exec(str));// ['76']
# (4)先行否定断言

x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/\d+(?!%)/

const reg = /\d+(?!\.)/
console.log(reg.exec('3.14'));  // ['14']

先行否定断言中括号的内容是不会被返回的

# 5、一些常用的正则

  1. 用户名

    只能包含数字字母下划线中划线 4-16位

    const userNameReg= /^[a-zA-Z0-9_-]{4,16}$/
    
  2. 密码强度

    最少6位,包括至少1个大写字母,1个小写字母,1个数字,1个特殊字符

    const psdReg = /^.*(?=.{6,})(?=.*\d)(?=.*[A-Z])(?=.*[a-z])(?=.*[!@#$%^&*? ]).*$/;
    const psd = 'A1Da123!'
    console.log(psdReg.test(psd));
    
  3. 整数正则

    包括正负数

    const intReg = /^-?\d+$/;
    
  4. 正负浮点数

    const floatReg = /^-?[1-9]{1,}\d*.\d+$/;
    

    如果想看一个字符串是不是一个格式正确的数字, 可以同时test intRegfloatReg

    也可以使用下面的正则, 包含0和正负数

    const numReg = /^[+-]?(0|([1-9]\d*))(\.\d+)?$/ 
    
  5. 邮箱

    const emailReg = /^([A-Za-z0-9_\-\.]+)(\@)([A-Za-z0-9_\-\.]+)\.([A-Za-z]+)$/;
    // 下面是邮箱名字可以包含中文的格式, 如果还允许其他字符, 可以继续加
    const emailWithChineseReg = /^([\u4e00-\u9fa5A-Za-z0-9_\-\.]+)(\@)([A-Za-z0-9_\-\.]+)\.([A-Za-z]+)$/;
    
  6. 电话号码

    // 11位手机号
    const mobilePhoneReg = /^1\d{10}$/
    // 座机 010-12345678 这种, 假设后面是8位, 区号可能是3位或者4位
    const landlinePhoneReg = /^0?\d{3}-\d{8}$/
    
  7. 十六进制颜色

    3位或者6位的

    const hexColorPattern = /^#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3})$/;
    
  8. 驼峰和下划线互相转

    // 驼峰转下划线
    function camelToUnderscore(str) {
      if (!str) return '';
      str = str[0].toLowerCase() + str.substring(1);
      return str.replace(/([A-Z])/g, '_$1').toLowerCase();
    }
    
    // 下划线转驼峰
    function underscoreToCamel(str) {
      return str.replace(/_([a-z])/g, (match, p1) => p1.toUpperCase());
    }
    
  9. 提取固定模式下的内容

    比如下面提取被大括号包括的东西

          const str = '上市日起{30}个{交易日}无交易时{每日}行情来源切换为{中债登}';
          const reg = /\{.*?\}/g;
          console.debug(str.match(reg)); //  ['{30}', '{交易日}', '{每日}', '{中债登}']
    
  10. ...

参考文档:https://javascript.ruanyifeng.com/stdlib/regexp.html (opens new window)https://www.bookstack.cn/read/bash-tutorial/docs-archives-regex.md (opens new window)

上次更新: 1/22/2025, 9:39:13 AM