正则表达式
基本概念
概念
正则表达式(Regular Expression),简称 regex,是一种特殊的文本模式(特定的规则、模板或结构),用于在字符串中搜索、匹配和替换文本。
正则表达式在计算机领域的发展
发展史
20 世纪 40 年代,两个神经生理学家研究出了一种用数学方式来描述神经网络的方法,可以将神经系统中的神经元描述成小而简单的自动控制元。
20 世纪 50 年代,Stephen Kleene 在以上两位的基础上发表了《神经网络事件表示法和有穷自动机》 论文。这篇论文描述了一种叫做 "正则集合(Regular Sets)" 的数学符号,引入了正则表达式的概念。
20 世纪 60 年代,Unix 之父 Ken Thompson 发表了 《正则表达式搜索算法》 论文。并应用在文本搜索工具 grep 中。
20 世纪 70 年代,由于 grep 支持的功能不多,出现了 egrep 、 awk 、 lex 、 sed 等,每个程序所支持的正则表达式都有差别。
20 世纪 80 年代,
POSIX
(Portable Operating System Interface)标准公布,其中就包含不同操作系统都需要遵守的正则表达式规范。20 世纪 90 年代,在 80 年代发布的 Perl 编程语言,引入了正则表达式功能,并随着语言的发展越来越强大,为了把 Perl 语言的正则表达式功能移植到其他语言中,诞生了开源项目
PCRE
(Perl Compatiable Regular Expressions)
正则表达式规范分类
遵循 POSIX 规则的正则表达式,称为 POSIX 派系的正则表达式。Unix 系统或类 Unix 系统上的大部分工具,如 grep 、sed 、awk 等都属于 POSIX 派系。
遵循 PCRE 规范的正则表达式,成为 PCRE 派系的正则表达式。现代编程语言如 Python , Ruby , PHP , C / C++ , Java 等正则表达式,大部分都属于 PCRE 派系,或参照 PCRE 派系发展而来。
POSIX 派系分为两个标准
BRE 标准(Basic Regular Expression 基本正则表达式)
ERE 标准(Extended Regular Expression 扩展正则表达式)
PCRE 跟 POSIX 相比有哪些优势?
PCRE 使用起来更加易用简洁(不需要转义,有更简洁字符组),功能更加丰富(非捕获组,环顾断言,非贪婪)。如果没有特殊原因,应尽可能使用 PCRE 派系,让正则匹配的结果更符合我们预期。
不同正则表达式规格和引擎的区别概览
基础语法
创建方式
RegExp 字面量
const reg = /hello/i
RegExp() 构造函数
const reg = new RegExp('hello', 'i')
new RegExp() 创建字面量形式
var regex = new RegExp(/xyz/i)
// 等价于
var regex = /xyz/i
其实还有一种形式的字面量 new RegExp(/xyz/i)
,注意第一个参数不是字符串,而是正则表达式字面量。
这样的写法就会存在正则表达式字面量中修饰符和 RegExp 构造函数第二个参数冲突的情况。在 ES5 中不允许此时使用第二个参数添加修饰符,否则会报错。
var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
ES6 改变了这种行为。如果 RegExp 构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。
new RegExp(/abc/ig).flags // gi
new RegExp(/abc/ig, 'i').flags // i ,原有正则对象的修饰符是ig,它会被第二个参数i覆盖
字面量和构造函数的区别
反斜杠
\
进行转义时,字面量语法需要一个反斜杠\
,而RegExp
构造函数需要两个反斜杠\\
(因为字符串内部会先转义一次)
例如:匹配字符串 1+1=2
const str = '1+2=3'
const reg1 = /1\+2=3/
const reg2 = new RegExp('1\\+2=3') // 两个反斜杠
reg1.test(str)
reg2.test(str)
以下是常见需要转义的字符:
反斜杠 (
\
):用于开始一个转义序列。点 (
.
):匹配除换行符之外的任何单个字符。星号 (
*
):表示零次或多次匹配前面的元素。加号 (
+
):表示一次或多次匹配前面的元素。问号 (
?
):表示零次或一次匹配前面的元素;也用于表示非贪婪匹配。圆括号 (
(
和)
):用于分组。方括号 (
[
和]
):用于定义字符集。花括号 (
{
和}
):用于指定数量限定符。竖线 (
|
):表示选择(或)。脱字符 (
^
):表示行的开始;在字符集中用来表示否定。美元符号 (
$
):表示行的结束。正斜杠 (
/
):如果你在/
之间使用正则表达式,也需要转义。
字面量语法在性能方面更有优势,如果正则表达式在代码中是固定的,不需要动态生成,那么使用字面量语法(如
/pattern/flags
)通常更高效,因为 JavaScript 引擎会在脚本加载时编译它们。相比之下,使用RegExp
构造函数创建的正则表达式可能会在每次使用时都重新编译。字面量语法更简洁方便和直观
RegExp
构造函数的最大的优势是能够动态地生成正则表达式。你可以在运行时根据变量或表达式构造一个正则表达式,这在使用字面量形式时做不到。
字面量字符
如果在正则表达式中,某个字符只表示它字面的含义,那他们就叫做字面量字符(Literal Characters)
例如下面代码中正则表达式的 d、 o 、g 三个字符,就是字面量字符
/dog/.test('old dog') // true
元字符
有一部分字符有特殊含义,不代表字面的意思,这些字符就是元字符(Meta Characters),元字符有以下几种:
反斜杠 (
\
):用于开始一个转义序列。点 (
.
):匹配除换行符之外的任何单个字符。星号 (
*
):表示零次或多次匹配前面的元素。加号 (
+
):表示一次或多次匹配前面的元素。问号 (
?
):表示零次或一次匹配前面的元素;也用于表示非贪婪匹配。圆括号 (
(
和)
):用于分组。方括号 (
[
和]
):用于定义字符集。花括号 (
{
和}
):用于指定数量限定符。竖线 (
|
):表示选择(或)。脱字符 (
^
):表示行的开始;在字符集中用来表示否定。美元符号 (
$
):表示行的结束。
转义符
正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配字符串 +
,就要写成 \+
。
正则表达式中,需要反斜杠转义的,一共有 12 个字符:反斜杠 (\
)、点 (.
)、星号 (*
)、加号 (+
)、问号 (?
)、圆括号 ((
和 )
)、方括号 ([
和 ]
)、花括号 ({
和 }
)、竖线 (|
)、脱字符 (^
)、美元符号 ($
)。需要特别注意的是,如果使用 RegExp 构造函数生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次。
怎么理解因为字符串内部会先转义一次
在字符串内部,反斜杠也是转义字符,所以它需要先被反斜杠转义一次。
var str = 'ab\c\*'
var str1 = 'ab\\c\\*'
console.log(str) // abc* 结果是不带转译符的
console.log(str1) // ab\c\*
点字符(.
)
.
)点字符(.
)匹配除回车()、换行() 、行分隔符(\u2028
)和段分隔符(\u2029
)以外的所有字符。
/c.t/.test('cat') // true
注意:对于码点大于 0xFFFF 的 Unicode 字符,点字符不能正确匹配,会认为这是两个字符。
选择符(|
)
|
)竖线符号(|
)在正则表达式中表示“或关系”(OR)。
选择符会包括它前后的多个字符,注意,是多个字符
比如/ab|cd/指的是匹配 ab 或者 cd,而不是指匹配 b 或者 c
'abd'.match(/ab|cd/g) // 返回 ['ab']
如果想要修改成单个字符,可以使用圆括号
'abd'.match(/a(b|c)d/g) // 返回 ['abd']
锚字符
锚字符包含两个位置字符和两个位置预定义模式
^
:匹配字符串的开头,在多行检索中,匹配一行的开头$
:匹配字符串的结尾,在多行检索中,匹配一行的结尾\b
:匹配词的边界\B
:匹配非词边界,即在词的内部
// \b 的例子
/\bworld/.test('hello world') // true
/\bworld/.test('hello-world') // true
/\bworld/.test('helloworld') // false
// \B 的例子
/\Bworld/.test('hello-world') // false
/\Bworld/.test('helloworld') // true
上面代码中,\b
表示词的边界,所以 world 的词首必须独立(词尾是否独立未指定),才会匹配。同理,\B
表示非词的边界,只有 world 的词首不独立,才会匹配。
其实零宽正向先行断言 (?=)
和零宽负向先行断言 (?!)
也属于锚字符的范畴。
量词
量词用来描述前面的相邻的(单个或一组)字符匹配规则出现的次数
{n,m}
:匹配前一项至少 n 次,但不能超过 m 次{n,}
:匹配前一项 n 次及以上{n}
:匹配前一项 n 次?
:匹配前一项 0 次或 1 次,也就是说前一项是可选的,等价于{0,1}
+
:匹配前一项 1 次或多次,等价于{1,}
*
:匹配前一项 0 次或多次,等价于{0,}
预定义模式
\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
:匹配非词边界,即在词的内部
扩展:跟 [^]
类似,[\S\s]
指代一切字符。
字符类
字符类(class)又被称为 字符组 或 字符集合,表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如 [xyz]
表示 x、y、z 之中任选一个匹配
方括号内的脱字符(^
)
^
)^
在方括号外用来匹配行的开头,但是在方括号内(开头第一个位置),则表示除了字符类之中的字符,其他字符都可以匹配。
//字符串bbc news包含a、b、c以外的其他字符,所以返回true
/[^abc]/.test('bbc news') // true
//字符串bbc不包含a、b、c以外的其他字符,所以返回false
/[^abc]/.test('bcc')
注意:如果方括号内没有其他字符,即只有[^]
,就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.
)是不包括换行符的。
var s = 'Please yes\nmake my day!';
s.match(/yes.*day/) // null
s.match(/yes[^]*day/) // [ 'yes\nmake my day']
注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义。
/[a^b]/.test('ccc^')// true
方括号内的连字符(-
)
-
)-
在方括号外匹配普通连字符号,但是在方括号内(不在开头),则表示字符的连续范围。比如,[abc]
可以写成 [a-c]
,[0123456789]
可以写成 [0-9]
,同理[A-Z]
表示 26 个大写字母。
/a-z/.test('b') // false
/[a-z]/.test('b') // true
以下都是合法的字符类简写形式
[0-9,]
[0-9a-fA-F]
[a-zA-Z0-9-] // 注意:最后一个 - ,只是代表普通连字符号
[1-31] //注意:不代表1到31,只代表1到3
连字符还可以用来指定 Unicode 字符的范围
var str = "\u0130\u0131\u0132";
/[\u0128-\uFFFF]/.test(str)
// true
注意:不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符
最典型的例子就是[A-z]
,表面上它是选中从大写的 A 到小写的 z 之间 52 个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果。
// 由于反斜杠(’\‘)的ASCII码在大写字母与小写字母之间,结果会被选中
/[A-z]/.test('\\') // true
如果想让方括号中的连字符匹配一个普通的连字符,连字符必须放在字符组的开头,保证它是一个普通字符。
方括号内的其他需要转义的元字符
需要转义的元字符,除了\
需要转义,不能直接写在方括号中外, . + ? * { } / ( ) $ | [ ]
在方括号内只是匹配普通字符
/[.+?*{}/()$|]/.test('*') // true
组匹配
组匹配相关的有分组字符、非捕获分组、引用字符
分组字符
正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容。
可通过 *
、+
、?
和 竖线|
等符号修饰
/hi+/.exec('hihi') // 只能匹配 'hi'
/(hi)+/.exec('hihi') // 可以匹配 'hihi'
可以进行分组捕获
// 正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c。
'abcabc'.match(/(.)b(.)/) // // ['abc', 'a', 'c']
注意:使用分组捕获时,不宜同时使用 g
修饰符,否则 match
方法不会捕获分组的内容。
'abcabc'.match(/(.)b(.)/g) // ['abc', 'abc']
上面代码使用带 g
修饰符的正则表达式,结果 match
方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的 exec
方法,配合循环,才能读到每一轮匹配的组捕获。
var str = 'abcabc'
var reg = /(.)b(.)/g
while (true) {
var result = reg.exec(str)
if (!result) break
console.log(result)
}
// ["abc", "a", "c"]
// ["abc", "a", "c"]
可以跟引用字符配合使用
/(.)b(.)\1b\2/.test("abcabc")
上面的代码中,\1 表示第一个括号匹配的内容(即 a),\2 表示第二个括号匹配的内容(即 c)
非捕获分组
(?:x)
称为非捕获组(Non-capturing group),表示不在捕获分组返回该组匹配的内容,即匹配的捕获分组的结果中不计入该分组的数据。
非捕获组的作用可能就是需要分组匹配,但是不需要在捕获分组中展示出来的情况。
// 一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的捕获分组的结果中没有第一个括号,只有第二个括号匹配的内容。
'abc'.match(/(?:.)b(.)/); //
注意:非捕获分组是不计入引用字符的编码的
引用字符
正则表达式内部,还可以用 引用括号匹配的内容,n 是从 1 开始的自然数,表示对应顺序的括号。
// \1表示第一个括号匹配的内容(即a),\2表示第二个括号匹配的内容(即c)
/(.)b(.)\1b\2/.test("abcabc") // true
// 引用字符的顺序可以按照实际情况调整
/y(..)(.)\2\1/.test('yabccab') // true
// 括号还可以嵌套,\1指向外层括号,\2指向内层括号
/y((..)\2)\1/.test('yabababab') // true
具名分组(ES2018)
ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
语法是:“具名组匹配” 在圆括号内部,模式的头部添加 “问号 + 尖括号 + 组名”,例如 ?<year>
捕获分组的改变
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如 matchObj[1]
)引用,要是组的顺序变了,引用的时候就必须修改序号。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/
const matchObj = RE_DATE.exec('1999-12-31')
const year = matchObj[1] // 1999
const month = matchObj[2] // 12
const day = matchObj[3] // 31
具名组匹配可以在 exec
方法返回结果的 groups
属性上引用该组名。同时,数字序号(matchObj[1]
)依然有效。
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const matchObj = RE_DATE.exec('1999-12-31')
const year = matchObj.groups.year // "1999"
const month = matchObj.groups.month // "12"
const day = matchObj.groups.day // "31"
具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。
如果具名组没有匹配,那么对应的 groups
对象属性会是 undefined
。
const RE_OPT_A = /^(?<as>a+)?$/
const matchObj = RE_OPT_A.exec('')
matchObj.groups.as // undefined
'as' in matchObj.groups // true
上面代码中,具名组 as 没有找到匹配,那么 matchObj.groups.as 属性值就是 undefined,并且 as 这个键名在 groups 是始终存在的。
引用字符的改变
具名组匹配的引用字符的语法是 \k
开始,后面添加“尖括号 + 组名”,例如 \k<first>
/(.)b(.)\1b\2/.test("abcabc") // true
/(?<first>.)b(?<last>.)\k<first>b\k<last>/.test("abcabc") // true
// 引用字符的顺序可以按照实际情况调整
/y(..)(.)\2\1/.test('yabccab') // true
/y(?<first>..)(?<last>.)\k<last>\k<first>/.test('yabccab') // true
replace 方法的改变
字符串替换时,使用$<组名>
引用具名组。
let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;
'2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
// '02/01/2015'
replace 方法的第二个参数也可以是函数,具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
'2015-01-02'.replace(re, (
matched, // 整个匹配结果 2015-01-02
capture1, // 第一个组匹配 2015
capture2, // 第二个组匹配 01
capture3, // 第三个组匹配 02
position, // 匹配开始的位置 0
S, // 原字符串 2015-01-02
groups // 具名组构成的一个对象 {year, month, day}
) => {
let {day, month, year} = groups;
return `${day}/${month}/${year}`;
});
实际应用的例子
匹配网页标签
let reg = /<([^>]+)>[^<]*<\/\1>/
捕获带有属性的标签
let reg = /<(\w+)\s([^>]*)>(.*?)<\/\1>/
匹配网址并且捕获 host
let reg = /^(https?:\/\/)?([\w-]+\.)+[\w-]+/
修饰符
修饰符(modifier)表示模式的附加规则,放在正则表达式的最尾部。
修饰符可以单个使用,也可以多个一起使用。
// 单个修饰符
var regex = /test/i;
// 多个修饰符
var regex = /test/ig;
g
修饰符
g
修饰符g
修饰符表示全局匹配(global)。默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换。
以下代码不含 g
修饰符,每次都是从字符串头部开始匹配。所以,连续做了三次匹配,都返回 true。
var regex = /b/;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // true
以下正则模式含有 g
修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串 abba 只有两个 b,所以前两次匹配结果为 true,第三次匹配结果为 false。
var regex = /b/g;
var str = 'abba';
regex.test(str); // true
regex.test(str); // true
regex.test(str); // false
i
修饰符
i
修饰符i
修饰符表示忽略大小写(ignorecase)。
m
修饰符
m
修饰符m
修饰符表示多行模式(multiline),会修改 ^
和 $
的行为。
默认情况下(即不加 m
修饰符时),^
和 $
匹配字符串的开始处和结尾处,加上 m
修饰符以后,^
和 $
还会匹配行首和行尾,即 ^
和 $
会识别换行符( )。
/world$/.test('hello world\n') // false
/world$/m.test('hello world\n') // true
/^b/m.test('a\nb') // true 加上m修饰符以后,换行符\n也会被认为是一行的开始
u
修饰符(ES6)
u
修饰符(ES6)含义为“Unicode 模式”,用来正确处理大于\uFFFF 的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。
点字符
点(.
)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于 0xFFFF
的 Unicode 字符,点字符不能识别,必须加上 u
修饰符。
var s = '𠮷';
/^.$/.test(s) // false
/^.$/u.test(s) // true
Unicode 字符表示法
ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上 u
修饰符,才能识别当中的大括号,否则会被解读为量词。
/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true
上面代码表示,如果不加 u
修饰符,正则表达式无法识别 \u{61}
这种表示法,只会认为这匹配 61 个连续的 u。
预定义模式
u
修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF
的 Unicode 字符。
/^\S$/.test('𠮷') // false
/^\S$/u.test('𠮷') // true
上面代码的 \S
是预定义模式,匹配所有非空白字符。只有加了 u
修饰符,它才能正确匹配码点大于 0xFFFF
的 Unicode 字符。
利用这一点,可以写出一个正确返回字符串长度的函数。
function codePointLength(text) {
var result = text.match(/[\s\S]/gu)
return result ? result.length : 0
}
var s = '𠮷𠮷'
s.length // 4
codePointLength(s) // 2
量词
使用 u 修饰符后,所有量词都会正确识别码点大于 0xFFFF 的 Unicode 字符。
/a{2}/.test('aa') // true
/a{2}/u.test('aa') // true
/𠮷{2}/.test('𠮷𠮷') // false
/𠮷{2}/u.test('𠮷𠮷') // true
转义
没有 u 修饰符的情况下,正则中没有定义的转义(如逗号的转义,)无效,而在 u 模式会报错。
/\,/ // 输出 /\,/
/\,/u // 报错
y
修饰符(ES6)
y
修饰符(ES6)也叫做“粘连”(sticky)修饰符。
y
修饰符的作用与 g
修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g
修饰符只要剩余位置中存在匹配就可,而 y
修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
var s = 'aaa_aa_a'
var r1 = /a+/g
var r2 = /a+/y
r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]
r1.exec(s) // ["aa"]
r2.exec(s) // null y修饰符要求匹配必须从头部开始,所以返回null
实际上,y
修饰符号隐含了头部匹配的标志 ^
。
/b/y.exec('aba') // null
单单一个 y
修饰符对 match
方法,只能返回第一个匹配,必须与 g
修饰符联用,才能返回所有匹配。
'a1a2a3'.match(/a\d/y) // ["a1"]
'a1a2a3'.match(/a\d/gy) // ["a1", "a2", "a3"]
s
修饰符:dotAll
模式(ES2018)
s
修饰符:dotAll
模式(ES2018)dotAll
模式,即点(dot)代表一切字符。
正则表达式中,点(.
)是一个特殊字符,代表任意的单个字符,
但是有两个例外:
一个是四个字节的 UTF-16 字符,这个可以用
u
修饰符解决;另一个是行终止符(line terminator character)。这个就需要
s
修饰符解决
所谓行终止符,就是该字符表示一行的终结。以下四个字符属于“行终止符”。
U+000A 换行符()
U+000D 回车符()
U+2028 行分隔符(line separator)
U+2029 段分隔符(paragraph separator)
/foo.bar/.test('foo\nbar') // false
上面代码中,因为 .
不匹配 ,所以正则表达式返回 false。
但是,很多时候我们希望匹配的是任意单个字符,这时有一种变通的写法。
/foo[^]bar/.test('foo\nbar') // true
这种解决方案毕竟不太符合直觉,ES2018 引入 s
修饰符,使得 .
可以匹配任意单个字符。
/foo.bar/s.test('foo\nbar') // true
s
修饰符和多行修饰符 m
不冲突,两者一起使用的情况下,.
匹配所有字符,而 ^
和$
匹配每一行的行首和行尾。
d
修饰符:正则匹配索引
d
修饰符:正则匹配索引ES2022 新增了 d 修饰符,这个修饰符可以让 exec()、match()的返回结果添加 indices 属性,在该属性上面可以拿到匹配的开始位置和结束位置。
注意,开始位置包含在匹配结果之中,相当于匹配结果的第一个字符的位置。但是,结束位置不包含在匹配结果之中,是匹配结果的下一个字符。
如果正则表达式包含组匹配,那么 indices 属性对应的数组就会包含多个成员,提供每个组匹配的开始位置和结束位置。
const text = 'zabbcdef';
const re = /ab+(cd)/d;
const result = re.exec(text);
result.indices // [ [ 1, 6 ], [ 4, 6 ] ]
上面例子中,正则表达式 re 包含一个组匹配(cd),那么 indices
属性数组就有两个成员,第一个成员是整个匹配结果(abbcd)的开始位置和结束位置,第二个成员是组匹配(cd)的开始位置和结束位置。
如果正则表达式包含具名组匹配,indices 属性数组还会有一个 groups 属性。该属性是一个对象,可以从该对象获取具名组匹配的开始位置和结束位置。
const text = 'zabbcdef';
const re = /ab+(?<Z>cd)/d;
const result = re.exec(text);
result.indices.groups // { Z: [ 4, 6 ] }
面例子中,exec()方法返回结果的 indices.groups 属性是一个对象,提供具名组匹配 Z 的开始位置和结束位置。
如果获取组匹配不成功,indices 属性数组的对应成员则为 undefined,indices.groups 属性对象的对应成员也是 undefined。
特殊字符
正则表达式对一些不能打印的特殊字符,提供了表达方法。
\cX
:表示Ctrl-[X]
,其中的 X 是 A-Z 之中任一个英文字母,用来匹配控制字符。[\b]
:匹配退格键(\u0008
),不要与\b
混淆:匹配换行键(\u000A)
:匹配回车键(\u000D)
:匹配制表符 tab(\u0009)
\v
:匹配垂直制表符(\u000B)\f
:匹配换页符(\u000C)\0
:匹配 null 字符(\u0000)\xhh
:匹配一个以两位十六进制数(\x00-\xFF)表示的字符\uhhhh
:匹配一个以四位十六进制数(\u0000-\uFFFF)表示的 Unicode 字符
其中有一些可以进行替代:
\x0A
等价于\u0009
等价于\cJ
等价于
属性方法
RegExp 实例方法
RegExp.prototype.test()
正则实例对象的 test
方法返回一个布尔值,表示当前模式是否能匹配参数字符串。
/cat/.test('cats and dogs') // true
正则表达式带有 g
修饰符
如果正则表达式带有 g 修饰符,则每一次 test 方法都从上一次结束的位置开始向后匹配。
var r = /x/g;
var s = '_x_x';
r.lastIndex // 0
r.test(s) // true
r.lastIndex // 2
r.test(s) // true
r.lastIndex // 4
r.test(s) // false
带有 g
修饰符时,可以通过正则对象的 lastIndex
属性指定开始搜索的位置。
var r = /x/g;
var s = '_x_x';
r.lastIndex = 4;
r.test(s) // false lastIndex 属性重置为 0
r.lastIndex // 0
r.test(s) // true
注意:带有 g
修饰符时,正则表达式内部会记住上一次的 lastIndex
属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误。
var r = /bb/g
r.test('bb') // true
// 由于正则表达式r是从上一次的lastIndex位置开始匹配,导致第二次执行test方法时出现预期以外的结果
r.test('-bb-') // false
注意:lastIndex 属性只对同一个正则表达式有效,所以下面这样写是错误的。
// 下面代码会导致无限循环,因为while循环的每次匹配条件都是一个新的正则表达式,导致lastIndex属性总是等于0。
var count = 0;
while (/a/g.test('babaa')) count++;
// 如果需要达到预期的效果,可以使用变量存放正则对象
var count = 0;
var reg = /a/g
while (reg.test('babaa')) count++;
如果正则模式是一个空字符串,则匹配所有字符串。
new RegExp('').test('abc')
如果想使用正则表达式字面量模式匹配所有字符串,不能用 //
,这个是错误的,需要使用 /(?:)/
/(?:)/.test('abc')
// 因为
new RegExp('').toString() // '/(?:)/'
另外 /^$/
可以匹配空字符串
/^$/.test('')
RegExp.prototype.exec()
正则实例对象的 exec
方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回 null
。
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
r1.exec(s) // ["x"] 其实返回的对象全部属性时这样的 0:"x" groups:undefined index:1 input:"_x_x" length: 1
r2.exec(s) // null
如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。 第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的 length 属性等于组匹配的数量再加 1。
var s = '_x_x';
var r = /_(x)/;
r.exec(s) // ["_x", "x"] length 是 2
返回值中的 input
属性和 index
属性
其实返回的数组全部属性时这样的 ['_x', 'x', index: 0, input: '_x_x', groups: undefined]
input
:整个原字符串。index
:整个模式匹配成功的开始位置(从 0 开始计数)。
var r = /a(b+)a/;
var arr = r.exec('_abbba_aba_');
arr // ["abbba", "bbb"]
arr.index // 1
arr.input // "_abbba_aba_"
如果正则表达式加上 g
修饰符
如果正则表达式加上 g
修饰符,则可以使用多次 exec
方法,下一次搜索的位置从上一次匹配成功结束的位置开始。
var reg = /a/g;
var str = 'abc_abc_abc'
while(true) {
var match = reg.exec(str);
if (!match) break;
console.log('#' + match.index + ':' + match[0]);
}
// #0:a
// #4:a
// #8:a
RegExp 对象属性
source
只读字符串,包含正则表达式的文本。
global
只读布尔值,是否带修饰符 g
ignoreCase
只读布尔值,是否带修饰符 i
multiline
只读布尔值,是否带修饰符 m
unicode
ES6 新增,只读布尔值,表示是否设置了 u 修饰符。
sticky
ES6 新增,只读布尔值,表示是否设置了 u 修饰符。
dotall
返回一个布尔值,表示该正则表达式是否处在 dotAll 模式。
flags
返回一个字符串,包含了已经设置的所有修饰符,按字母排序。
var r = /abc/gimuy
r.ignoreCase // true
r.global // true
r.multiline // true
r.unicode // true
r.sticky // true
r.flags // 'gimuy'
lastIndex
可读写整数,如果带 g 修饰符,这个属性储存在整个字符串中下一次检索开始的位置,这个属性会被 exec()
和 test()
方法用到。
当调用 exec()
或 test()
的正则表达式具有修饰符 g
时,它将把当前正则表达式对象的 lastIndex
属性设置为紧挨着匹配子串的字符位置。如果没发现任何匹配结果,lastIndex
将重置为 0
。
字符串实例方法
String.prototype.match()
对字符串进行正则匹配,匹配成功返回一个数组,成员是所有匹配的子字符串,匹配失败返回 null。
var s = '_x_x';
var r1 = /x/;
var r2 = /y/;
s.match(r1) // ["x"]
s.match(r2) // null
如果正则表达式带有 g 修饰符
如果正则表达式带有 g
修饰符,则该方法与正则对象的 exec
方法行为不同,会一次性返回所有匹配成功的结果。
var s = 'abba';
var r = /a/g;
s.match(r) // ["a", "a"]
r.exec(s) // ["a"]
设置正则表达式的 lastIndex
属性,对 match
方法无效,匹配总是从字符串的第一个字符开始
var r = /a|b/g;
r.lastIndex = 7;
'xaxb'.match(r) // ['a', 'b']
r.lastIndex // 0
String.prototype.matchAll()
ES2020 增加了 String.prototype.matchAll()方法,可以一次性取出所有匹配。不过,它返回的是一个迭代器(Iterator),而不是数组。
const string = 'test1test2test3'
const regex = /t(e)(st(\d?))/g
for (const match of string.matchAll(regex)) {
console.log(match)
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
上面代码中,由于 string.matchAll(regex)
返回的是迭代器,所以可以用 for...of
循环取出。相对于返回 match()
数组,返回迭代器的好处在于,如果匹配结果是一个很大的数组,那么迭代器比较节省资源。
迭代器转为数组是非常简单的,使用...
运算符和 Array.from()
方法就可以了。
// 转为数组的方法一
[...string.matchAll(regex)]
// 转为数组的方法二
Array.from(string.matchAll(regex))
String.prototype.search()
返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1
'_x_x'.search(/x/) // 1
String.prototype.replace()
字符串对象的 replace
方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容。
正则表达式由 g
修饰符
正则表达式如果不加 g
修饰符,就替换第一个匹配成功的值,如果有替换所有匹配成功的值。
'aaa'.replace('a', 'b') // "baa"
'aaa'.replace(/a/, 'b') // "baa"
'aaa'.replace(/a/g, 'b') // "bbb"
第二个参数可以使用美元符号 $
replace
方法的第二个参数可以使用美元符号 $
,用来指代所替换的内容。
$&
:匹配的子字符串。$`:匹配结果前面的文本。
$'
:匹配结果后面的文本。$n
:匹配成功的第 n 组内容,n 是从 1 开始的自然数。$$
:指代美元符号$
。
'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1')
// "world hello"
'abc'.replace('b', '[$`-$&-$\'$$]')
// 'a[a-b-c$]c'
第二个参数还可以是一个函数
replace
方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值
作为 replace
方法第二个参数的替换函数,可以接受多个参数。
第一个参数是捕捉到的内容
第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。
此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置,最后一个参数是原字符串。
'6 and 10'
'3 and 5'.replace(/[0-9]+/g, function (...args) {
console.log(args)
return 2 * args[0];
})
//'6 and 10'
// ['3', 0, '3 and 5'] 注意倒数第二个和倒数第一个参数
// ['5', 6, '3 and 5']
replace 应用
将价格插入到模板中
var prices = {
'p1': '$1.99',
'p2': '$9.99',
'p3': '$5.00'
};
var template = '<span id="p1"></span>'
+ '<span id="p2"></span>'
+ '<span id="p3"></span>';
template.replace(
/(<span id=")(.*?)(">)(<\/span>)/g,
function(match, $1, $2, $3, $4){
return $1 + $2 + $3 + prices[$2] + $4;
}
);
消除字符串首尾两端的空格
function customTrim(str) {
// 正则表达式:匹配字符串开头和结尾的空白字符
// ^\s+ 匹配字符串开头的一个或多个空白字符
// \s+$ 匹配字符串结尾的一个或多个空白字符
const regExp = /^\s+|\s+$/g
// 使用 replace 方法和正则表达式移除字符串两端的空白字符
return str.replace(regExp, '')
}
String.prototype.split()
字符串对象的 split 方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组。
// 非正则分隔
'a, b,c, d'.split(',')
// [ 'a', ' b', 'c', ' d' ]
// 正则分隔,去除多余的空格
'a, b,c, d'.split(/, */)
// [ 'a', 'b', 'c', 'd' ]
// 指定返回数组的最大成员
'a, b,c, d'.split(/, */, 2)
[ 'a', 'b' ]
贪婪匹配下的分割
// 例一
'aaa*a*'.split(/a*/)
// [ '', '*', '*' ]
// 例二
'aaa**a*'.split(/a*/)
// ["", "*", "*", "*"]
上面代码的分割规则是 0 次或多次的 a,由于正则默认是贪婪匹配,所以例一的第一个分隔符是 aaa,第二个分割符是 a,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是 aaa,第二个分隔符是 0 个 a(即空字符),第三个分隔符是 a,所以将字符串分成四个部分。
如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回
'aaa*a*'.split(/(a*)/)
// [ '', 'aaa', '*', 'a', '*' ]
ES6 的扩展
ES6 出现之前,字符串对象共有 4 个方法,可以使用正则表达式:match()
、replace()
、search()
和split()
。
ES6 将这 4 个方法,在语言内部全部调用 RegExp 的实例方法,从而做到所有与正则相关的方法,全都定义在 RegExp 对象上。
String.prototype.match
调用RegExp.prototype[Symbol.match]
String.prototype.replace
调用RegExp.prototype[Symbol.replace]
String.prototype.search
调用RegExp.prototype[Symbol.search]
String.prototype.split
调用RegExp.prototype[Symbol.split]
例如:
var re = /[0-9]+/g
var str = '2022-01-02'
var result = re[Symbol.match](str)
console.log(result) // ["2022", "01", "02"]
进阶概念
匹配模式
贪婪模式
正则表达式的匹配模式,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式。
var s = 'aaa';
s.match(/a+/) // ["aaa"]
上面代码中,模式是 /a+/
,表示匹配 1 个 a 或多个 a,那么到底会匹配几个 a 呢?因为默认是贪婪模式,会一直匹配到字符 a 不出现为止,所以匹配结果是 3 个 a。
非贪婪模式
非贪婪模式又称懒惰模式,只需要在待匹配的量词字符后面跟随一个问号即可:??
、+?
、*?
、{1,5}?
,一旦条件满足,就不再往下匹配。
var s = 'aaa';
s.match(/a+?/) // ["a"]
再举一个例子
'aabab'.match(/a.*?b/g) // ['aab', 'ab']
a.*?b
匹配最短的,以 a 开始,以 b 结束的字符串。把它应用于 aabab 的话,它会匹配 aab(第一到第三个字符)和 ab(第四到第五个字符)。
为什么第一个匹配是 aab(第一到第三个字符)而不是 ab(第二到第三个字符)?简单地说,因为正则表达式有另一条规则,比懒惰/贪婪规则的优先级更高:最先匹配到的拥有最高的优先权——The match that begins earliest wins。
独占模式(JS 暂不支持)
独占模式需要在待匹配的量词字符后面跟随一个加号即可:?+
、++
、*+
、{1,5}+
。同贪婪模式一样,独占模式一样会匹配最长。不过在独占模式下,正则表达式尽可能长地去匹配字符串,一旦匹配不成功就会结束匹配而不会回溯。
匹配顺序
通常情况下,正则将会从左到右地测试每个条件。
使用分枝条件时,要注意各个条件的顺序。
如果满足了某个分枝的话,就不会去再管其它的条件了。
例如:美国邮编的规则是 5 位数字,或者用连字号间隔的 9 位数字。
/\d{5}-\d{4}|\d{5}/ // 正确方式
/\d{5}|\d{5}-\d{4}/ // 错误方式,只会匹配5位的邮编(以及9位邮编的前5位)
断言
断言(Assertion)也被称为零宽断言或环视(lookaround),它代表的是一个位置,这个位置应该满足一定的条件(断言),只有当断言为真时才会认为其之前或之后的正则匹配成功。
它自身只进行子表达式的匹配,不占有字符,匹配到的内容不保存到最终的匹配结果。
先行断言
先行断言(lookahead)有时也被称作正向断言,语法是:(?=exp)
,用于检查某个特定模式是否出现在另一个模式的后面(右边),通俗点说,在某个位置往右(往后)看是否匹配 ?=
后面的子表达式。
var m = 'abcabd'.match(/b(?=c)/) // ["b"]
上面代码中,匹配 b 的地方有两个,然后从这两个位置往右看是否匹配正向断言的子表达式 c,所以只有第一个 b 是真正匹配成功的。
密码强度校验(举例)
至少有一个大写字母。至少有一个小写字母。至少有一个数字。至少有 8 个字符。
/^(?=.*[A-Z])(?=.*[a-z])(?=.*\d).{8,}$/.test('123456aA') // true
这是一个特殊的例子,用于检查字符串中是否至少有一个大写字母等。
(?=.*[A-Z])
:用于检查字符串中是否存在至少一个大写字母。其中:
?=
是先行断言的开始。.*
代表任意数量的任意字符,这意味着大写字母可以出现在字符串中的任意位置。[A-Z]
匹配任意一个大写字母。
先行否定断言
先行否定断言(negative lookahead)有时也被称为正向否定断言,语法是:(?!=exp)
,用于确保某个模式后面不跟随另一个特定的模式
/\d+(?!\.)/.exec('3.14') // ["14"]
上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是 14。
数字格式化(举例)
1234567890 格式化为 1,234,567,890
let test = '1234567890'
let reg = /\B(?=(\d{3})+(?!\d))/g
console.log(test.replace(reg, ','))
主要看一下正则表达式部分:
\B
非词边界,即在词内部的间隔处?=
先行断言,后面必须匹配(\d{3})+
1 个或多个连续的三个数字?!
先行否定断言,多个连续的三个数字后不能有数字
后行断言
后行断言(lookbehind)有时也被称为反向断言,语法是:(?<=exp)
,用于检查某个特定模式是否出现在另一个模式的前面(左边)。
/(?<=\$)\d+/.exec('123$100') // 100
“后行断言”的实现,需要先匹配 /(?<=y)x/
的 x
,然后再回到左边,匹配 y
的部分。这种“先右后左”的执行顺序(后行),与所有其他正则操作相反,导致了一些不符合预期的行为。
后行断言的组匹配
/^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
没有“后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是 105 和 3。
/(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
有“后行断言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是 1 和 053。
后行断言的引用字符
如果后行断言的反斜杠引用字符(\1
)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。
/(?<=(o)d\1)r/.exec('hodor') // null
/(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
后行否定断言
后行否定断言(negative lookbehind)有时也被称为反向否定断言,语法是:(?<!exp)
,用于确保某个模式前面不是另一个特定的模式。
/(?<!\$)\d+/.exec('123$100') // 123
正则表达式引擎
正则表达式的执行需要正则引擎,正则引擎主要分为两类:
DFA(Deterministic finite automaton) 确定型有穷自动机
使用 DFA 引擎的程序:awk(大多数版本)、egrep(大多数版本)、flex、lex、MySQL 等
NFA(Non-deterministic finite automaton)不确定型有穷自动机
使用 NFA 引擎的程序:Java、Perl、PCRE library、less、more、sed(大多数版本)、Python、Ruby 等
一般指的是 Traditional NFA
其实:NFA 还能继续分为 Traditional NFA 和 POSIX NFA,这就跟 PCRE 流派和 POSIX 流派有些关系了,另外还有一些程序支持 DFA 和 NFA 的结合体,例如 GNU awk
确定型和不确定型
假设有一个字符串 abc 需要匹配,在没有编写正则表达式的前提下,直接可以确定字符匹配顺序的就是确定型,不能确定字符匹配顺序的就是不确定型。
有穷
有穷即表示有限的意思,这里表示有限的次数内能得到结果
自动机
自动机就是自动完成,在我们设置好匹配规则后由引擎会自动完成,不需要人为干预。
DFA 引擎
DFA 是从匹配文本入手,从左到右,每个字符不会匹配两次,它的时间复杂度是多项式的,所以通常情况下,它的速度更快,但支持的特性很少,不支持捕获组、各种引用等等
正则里面的 DFA 引擎实际上就是把正则表达式转换成一个图的邻接表,然后通过跳表的形式判断一个字符串是否匹配该正则。
DFA 的特点
先看文本,再看正则,以文本为主导
匹配过程,字符串只看一次,不管正则表达式写得多烂,匹配速度都很快
不支持捕获组、断言等高级功能
NFA 引擎
NFA 是从正则表达式入手,不断读入字符,尝试是否匹配当前正则,不匹配则吐出字符重新尝试,通常它的速度比较慢,最优时间复杂度为多项式的,最差情况为指数级的。但 NFA 支持更多的特性,因而绝大多数编程场景下(包括 Java,JS),就是 NFA。
正则里面 NFA 引擎实际上就是在语法解析的时候,构造出的一个有向图。然后通过深搜的方式,去一条路径一条路径的递归尝试。
NFA 的特点
先看正则,再看文本,以正则为主导
匹配过程中,可能会发生回退,字符串同一部分会比较多次(通常将回退称为“回溯”)
功能强大,可以拿到匹配的上下文信息,支持捕获组、断言等功能
灾难性回溯真实案例
大部分语言的正则引擎都是 NFA 的,JS 也是,如果写出了有性能问题的正则表达式,容易造成灾难性回溯。
使用 regex101 统计 step
let regexp = /^(\w+\s?)*$/;
alert( regexp.test("A good string") ); // true 一共 14 steps
alert( regexp.test("Bad characters: $@#") ); // false 一共 16412 steps,如果字符串再长一点会执行更多 step
如何避免
改用 DFA 的正则引擎(速度快,功能弱,没有捕获组断言等功能)
提高对正则性能问题的重视,开发的时候少写模糊匹配,越精确越好,因为模糊匹配、贪婪匹配、懒惰匹配都可能带来回溯问题
使用独占模式可以有效避免回溯问题(JS 暂时没有此模式)
不要滥用括号和字符类
拆分表达式,有时候,多个小正则表达式的速度比一个大正则表达式的速度要快
性能
基本概述
正则表达式的性能高于常规的循环遍历字符串操作
如果只需要匹配几个字母的大小写,可以直接写明,如果使用 i
修饰符,会每个字母都匹配一遍大小写
i
修饰符,会每个字母都匹配一遍大小写/[MmSs]/.test('mS')
尽量使用正则表达式字面量形式
因为 JavaScript 引擎会在脚本加载时编译正则表达式字面量。相比之下,使用 RegExp
构造函数创建的正则表达式可能会在每次使用时都重新编译
使用行或者字符串的开始、结束符
如果是从行首或者行尾匹配,使用 ^
或 $
能更精准匹配
扩展
匹配中文字符
[\u4e00-\u9fa5]
是错的,不要用二十年前的正则表达式了/\p{Unified_Ideograph}/u
是正确的,不需要维护,匹配所有汉字。这里\p
是 Unicode 属性转义正则表达式,并且需要使用u
修饰符。/\p{Ideographic}/u
和/\p{Script=Han}/u
匹配除了汉字以外的其他一些字符,太宽泛,在「汉字匹配正则表达式」这个需求下,是错的。目前只有 Chrome 支持 Unicode 属性转义正则表达式。对其他环境,使用 @babel/plugin-proposal-unicode-property-regex 和 regexpu-core 进行优雅降级。
具体细节可以看
链接
技术文章
验证和联系
Last updated
Was this helpful?