正则表达式
正则表达式(Regular Expression),在代码中常简写为 regex、regexp或RE。使用单个字符串来描述、匹配一系列符合某个句法规则的字符串搜索模式。
搜索是可用于文本搜索和文本替换。
语法:
/正则表达式主体/修饰符(可选)
在 javascript 中, 正则表达式通常用于两个字符串方法:search()
和 replace()
。
search()
方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。
replace()
方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
-
search()
方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.search(/ello/)) // 1
console.log(str.search(/lls/)) // -1
// 字符串:字符串参数会转换为正则表达式
console.log(str.search('ello')) // 1
console.log(str.search('lls')) // -1
-
replace()
方法的使用
const str = 'hello world'
// 正则表达式
console.log(str.replace(/ello/, 'ey')) // hey world
console.log(str.replace(/elll/, 'ey')) // hello world
// 字符串:字符串参数会转换为正则表达式
console.log(str.replace('ello', 'ey')) // hey world
console.log(str.replace('elll', 'ey')) // hello world
正则表达式修饰符
修饰符可以在全局搜索中不区分大小写
修饰符 | 描述 |
i | 执行对大小写不敏感的匹配 |
g | 执行全局匹配(查找所有匹配而非在找到第一个匹配后停止) |
m | 执行多行匹配 |
const str = 'hElLo world'
console.log(str.replace(/ello/, 'ey')) // hElLo world
console.log(str.replace(/ello/i, 'ey')) // hey world
const str2 = 'hello world\nRegExp'
console.log(str2.replace(/^Reg/, 'reg')) // hello world\nRegExp
console.log(str2.replace(/^Reg/m, 'reg')) // hello world\nregExp
const str3 = 'hello world hello RegExp'
console.log(str3.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str3.replace(/ello/g, 'ey')) // hey world hey RegExp
正则表达式模式
方括号
方括号用于查找某个范围内的字符
表达式 | 描述 |
[abc] | 查找方括号之间的任何字符 |
[^abc] | 查找任何不在方括号之间的字符 |
[0-9] | 查找任何从0至9的数字 |
[a-z] | 查找任何从小写a到小写z的字符 |
[A-Z] | 查找任何从大写A到大写Z的字符 |
[A-z] | 查找任何从大写A到小写z的字符 |
元字符
元字符是拥有特殊含义的字符
元字符 | 描述 |
| 查找单个字符,除了换行和行结束符 |
\w | 查找单词字符 [a-zA-Z_0-9] |
\W | 查找非单词字符 |
\d | 查找数字 |
\D | 查找非数字字符 |
\s | 查找空白字符 |
\S | 查找非空白字符 |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
\0 | 查找NULL字符 |
\n | 查找换行符 |
\f | 查找换页符 |
\r | 查找回车符 |
\t | 查找制表符 |
\v | 查找垂直制表符 |
\xxx | 查找以八进制数xxx规定的字符 |
\xdd | 查找以十六进制数 dd 规定的字符 |
\uxxxx | 查找以十六进制数 xxxx 规定的 Unicode 字符 |
量词
量词 | 描述 |
n+ | 匹配任何包含至少一个 n 的字符串 |
n* | 匹配任何包含零个或多个 n 的字符串 |
n? | 匹配任何包含零个或一个 n 的字符串 |
n{X} | 匹配包含 X 个 n 的序列的字符串 |
n{X, Y} | 匹配包含 X 至 Y 个 n 的序列的字符串 |
n{X, } | 匹配包含至少 X 个 n 的序列的字符串 |
n$ | 匹配任何结尾为 n 的字符串 |
^n | 匹配任何开头为 n 的字符串 |
?=n | 匹配任何其后紧接指定字符串 n 的字符串 |
?!n | 匹配任何其后没有紧接指定字符串 n 的字符串 |
RegExp 对象方法
方法 | 描述 |
compile | 编译正则表达式 |
exec | 检索字符串中指定的值,返回找到的值,并确定其位置 |
test | 检索字符串中指定的值,返回 true 或 false |
const str = 'hello world hello RegExp'
console.log(/ello/.compile(/ello/)) // /ello/
console.log(/ello/.exec(str)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]
console.log(/ello/.test(str)) // true
test
方法的坑
test
方法用于测试字符串参数中是否存在匹配正则表达式模式的字符串。
lastIndex
:是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。
当正则表达式使用了全局匹配时,test
方法会出现奇怪现象:
let reg = /\w/g
console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // true
console.log(reg.test('ab')) // false
console.log(reg.test('ab')) // true
正常情况,ab
符合 test
方法,都应该返回 true
才对,原因就在于 lastIndex
属性。
我们可以试试每次都运行 test
方法打印出 lastIndex
的值:
let reg = /\w/g
console.log(reg.lastIndex) // 0
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 2
console.log(reg.test('ab')) // false
console.log(reg.lastIndex) // 0
console.log(reg.test('ab')) // true
console.log(reg.lastIndex) // 1
lastIndex
属性是当前表达式匹配内容的最后一个字符的后一位,用于规定下一次匹配的起始位置。
当进入正则表达式全局模式时,每次使用 test
方法都会从 lastIndex
开始,匹配从 lastIndex
开始的子字符串。比如例子中,第二次执行 test
方法时,此时,lastIndex
已经变为2,子字符串为空,所以 reg
不可能匹配上它,由于子字符串匹配失败,test
方法返回 false
,并将 lastIndex
属性置为0,重新开始一轮循环。
避免 test
中的坑的方法:
-
test
方法本身就是用来测试是否存在匹配正则的字符串,不使用全局模式一样可以实现目的,所以第一种方法就是不适用全局模式; - 不将正则对象实例存在变量中,每次直接用正则对象实例调用
test
方法,不过这种方法对内存有所损耗,理论上不建议。
支持正则表达式的 String 对象的方法
方法 | 描述 |
search | 检索与正则表达式相匹配的值 |
match | 找到一个或多个正则表达式的匹配 |
replace | 替换与正则表达式匹配的子串 |
split | 把字符串分割为字符串数组 |
const str = 'hello world hello RegExp'
console.log(str.search(/ello/)) // 1
console.log(str.search(/Ello/)) // -1
console.log(str.match(/ello/)) // [ 'ello', index: 1, input: 'hello world hello RegExp', groups: undefined ]
console.log(str.replace(/ello/, 'ey')) // hey world hello RegExp
console.log(str.replace(/Ello/, 'ey')) // hello world hello RegExp
console.log(str.split(/ello/)) // [ 'h', ' world h', ' RegExp' ]
console.log(str.split(/ello/, '1')) // [ 'h' ]
console.log(str.split(/ello/, 2)) // [ 'h', ' world h' ]
console.log(str.split(/ello/, 3)) // [ 'h', ' world h', ' RegExp' ]
search 方法
String.prototype.search(reg)
search
方法用于检索字符串中指定的子字符串,或检索与正则表达式相匹配的子字符串,并返回子串的起始位置。方法返回第一个匹配结果的 index
。查找不到则返回 -1
。
-
search
方法不执行全局匹配,它将忽略修饰符g
,并且总是从字符串的开始进行检索,因此,它不会产生类似于test
方法的问题。 - 不输入正则表达式则
search
方法将会自动将其转为正则表达式。
match 方法
String.prototype.match(reg)
match
方法将检索字符串,以找到一个或多个与 reg
匹配的文本,reg
是否具有修饰符 g
对结果影响很大。
非全局调用:
如果 reg
没有修饰符 g
,那 match
方法就只能在字符串中执行一次匹配,如果没有找到任何匹配的文本,将返回 null
。否则,它将返回一个数组,其中存放了与它找到的匹配文本有关的信息。
返回的数组的第一个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。
除了常规的数组元素外,返回的数组还含有2个对象属性:
- index:声明匹配文本的起始字符在字符串的位置
- input:声明对
stringObject
的引用
let str = 'af12131ds22'
console.log(str.match('v')) // null
console.log(str.match('s')) // [ 's', index: 8, input: 'af12131ds22', groups: undefined ]
全局调用:
如果 reg
具有修饰符 g
,则 match
方法将执行全局检索,找到字符串中的所有匹配子字符串。如果没有找到任何匹配的子串,将返回 null
。否则,返回一个数组。数组元素中存放的是字符串中所有匹配到的子串,而且也没有 index
属性或 input
属性。
let str = 'af12131ds22'
console.log(str.match(/[^\w]/g)) // null
console.log(str.match(/\d+/g)) // [ '12131', '22' ]
split 方法
String.prototype.split(reg)
把字符串分割为字符数组
let str = 'af12131ds22'
console.log(str.split('')) // [ 'a', 'f', '1', '2', '1', '3', '1', 'd', 's', '2', '2' ]
console.log(str.split('1')) // [ 'af', '2', '3', 'ds22' ]
一些复杂情况可以使用正则表达式解决
let str = 'af12131ds22'
console.log(str.split(/\d/)) // [ 'af', '', '', '', '', 'ds', '', '' ]
console.log(str.split(/\d+/)) // [ 'af', 'ds', '' ]
replace 方法
String.prototype.replace()
replace
方法有三种形态:
String.prototype.replace(str, replaceStr)
String.prototype.replace(reg, replaceStr)
String.prototype.replace(reg, function)
let str = 'af12131ds22'
console.log(str.replace('1', 7)) // af72131ds22
console.log(str.replace(/1/g, 7)) // af72737ds22
str.replace(/1/g, function (...args) {
console.log(args)
})
/**
* [ '1', 2, 'af12131ds22' ]
* [ '1', 4, 'af12131ds22' ]
* [ '1', 6, 'af12131ds22' ]
*/
replace
接受函数参数时,有四个参数:
- 匹配字符串
- 正则表达式分组内容,没有分组则没有该参数
- 匹配项在字符串中的
index
- 原字符串
// 有分组内容的
let str = 'af12131ds22'
str.replace(/(\d)(\w)/g, function (...args) {
console.log(args)
})
/**
* [match, group1, group2, index, origin]
[ '12', '1', '2', 2, 'af12131ds22' ]
[ '13', '1', '3', 4, 'af12131ds22' ]
[ '1d', '1', 'd', 6, 'af12131ds22' ]
[ '22', '2', '2', 9, 'af12131ds22' ]
*/
let str = 'af12131ds22'
str.replace(/(\d)(\w)(\d)/g, function (...args) {
console.log(args)
})
/**
* [match, group1, group2, group3, index, origin]
[ '121', '1', '2', '1', 2, 'af12131ds22' ]
*/
后行断言
JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead)。不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言。
量词 | 描述 |
?=n | 匹配任何其后紧接指定字符串 n 的字符串 |
?!n | 匹配任何其后没有紧接指定字符串 n 的字符串 |
“先行断言”指的是,x
只有在 y
前面才匹配,必须写成 /x(?=y)/
。比如,只匹配百分号之前的数字,要写成 /\d+(?=%)/
。
“先行否定断言”指的是,x
只有不在 y
前面才匹配,必须写成/x(?!y)/
。比如,只匹配不在百分号之前的数字,要写成 /\d+(?!%)/
。
let lookbehead = /\d+(?=%)/.exec('100% of US presidents have been male')
console.log(lookbehead)
let negativeLookbehead = /\d+(?!%)/.exec('that is all 44 of them')
console.log(negativeLookbehead)
/**
* [
'100',
index: 0,
input: '100% of US presidents have been male',
groups: undefined
]
[ '44', index: 12, input: 'that is all 44 of them', groups: undefined ]
*/
上面两个字符串,如果互换正则表达式,就不会得到相同的结果。另外,还可以看到,“先行断言”括号之中的部分((?=%)
),是不计入返回结果的。
“后行断言”正好与“先行断言”相反,x
只有在 y
后面才匹配,必须写成 /(?<=y)x/
。比如,只匹配美元符号后面的数字,要写成/(?<=\$)\d+/
。
“后行否定断言”则与“先行否定断言”相反,x
只有不在y
后面才匹配,必须写成 /(?<!y)x/
。比如,只匹配不在美元符号后面的数字,要写成 /(?<!\$)\d+/
。
let lookbehind = /(?<=\$)\d+/.exec('$100 of US presidents have been male')
console.log(lookbehind)
let negativeLookbehind = /(?<!\$)\d+/.exec('that is all &44 of them')
console.log(negativeLookbehind)
/**
* [
'100',
index: 1,
input: '$100 of US presidents have been male',
groups: undefined
]
[
'44',
index: 13,
input: 'that is all &44 of them',
groups: undefined
]
*/
上面例子中,“后行断言”的括号之中的部分((?<=\$)
),也是不计入返回结果。
下面的例子是使用后行断言进行字符串替换。
let reg = /(?<=\$)foo/g
console.log('$foo %foo foo'.replace(reg, 'bar')) // $bar %foo foo
上面代码中,只有在美元符号后面的 foo
才会被替换。
“后行断言”的实现,需要先匹配 /(?<=y)x/
的 x
,然后再回到左边,匹配 y
的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。
具名组匹配
正则表达式使用圆括号进行组匹配
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/
上面代码中,正则表达式里面有三组圆括号。使用 exec
方法,就可以将这三组匹配结果提取出来。
const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/
const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: undefined ]
const year = matchObj[1] // 1997
const month = matchObj[2] // 11
const day = matchObj[3] // 26
组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如 matchObj[1]
)引用,要是组的顺序变了,引用的时候就必须修改序号。
ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const matchObj = RE_DATE.exec('1997-11-26')
console.log(matchObj)
// [ '1997-11-26', '1997', '11', '26', index: 0, input: '1997-11-26', groups: [Object: null prototype] { year: '1997', month: '11', day: '26' } ]
const year = matchObj.groups.year // 1997
const month = matchObj.groups.month // 11
const day = matchObj.groups.day // 26
上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号+尖括号+组名”(?<year>
),然后就可以在 exec
方法返回结果的 groups
属性上引用该组名。同时,数字序号 (matchObj[1]
) 依然有效。
具名组匹配等于为每一组匹配加上了ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。
如果具名组没有匹配,那么对应的 groups
对象属性会是 undefined
。
const reg = /^(?<as>a+)?$/
const matchObj = reg.exec('')
console.log(matchObj)
console.log(matchObj.groups.as) // undefined
console.log('as' in matchObj.groups) // true
/**
* [
'',
undefined,
index: 0,
input: '',
groups: [Object: null prototype] { as: undefined }
]
*/
上面代码中,具名组 as
没有找到匹配,那么 matchObj.groups.as
属性值就是 undefined
,并且 as
这个键名在 groups
是始终存在的。
解构赋值和替换
有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。
let { groups: { one, two } } = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar')
console.log(one, two) // foo bar
字符串替换时,使用 $<组名>
引用具名组。
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
console.log('2006-06-07'.replace(reg, '$<day>/$<month>/$<year>')) // 07/06/2006
上面代码中,replace
方法的第二个参数是一个字符串,而不是正则表达式。
replace
方法的第二个参数也可以是函数,该函数的参数序列如下。
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
let str = '2015-01-02'.replace(reg, (
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}`
})
console.log(str) // 02/01/2015
具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
let reg = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u
let str1 = '2015-01-02'.replace(reg, (...args) => {
console.log(args)
let { day, month, year } = args[6]
return `${day}/${month}/${year}`
})
console.log(str1)
/**
* [
'2015-01-02',
'2015',
'01',
'02',
0,
'2015-01-02',
[Object: null prototype] { year: '2015', month: '01', day: '02' }
]
*/
引用
如果要在正则表达式内部引用某个“具名组匹配”,可以使用 \k<组名>
的写法。
const reg = /^(?<word>[a-z]+)!\k<word>$/
console.log(reg.test('abc!abc')) // true
console.log(reg.test('abc!ab')) // false
数字引用(\1
)依然有效。
const reg = /^(?<word>[a-z]+)!\1$/
console.log(reg.test('abc!abc')) // true
console.log(reg.test('abc!ab')) // false
这两种引用方法还可以同时使用。
const reg = /^(?<word>[a-z]+)!\k<word>!\1$/
console.log(reg.test('abc!abc!abc')) // true
console.log(reg.test('abc!ab!ab')) // false
String.prototype.matchAll()
如果一个正则表达式在字符串里面有多个匹配,现在一般使用 g
修饰符或 y
修饰符,在循环里面逐一取出。
let regex = /t(e)(st(\d?))/g
let string = 'test1test2test3'
let matches = []
let match
while (match = regex.exec(string)) {
matches.push(match)
}
console.log(matches)
// [
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined],
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined],
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]
// ]
console.log(reg.exec(str))
// [ 'test1', 'e', 'st1', '1', index: 0, input: 'test1test2test3', groups: undefined ]
上面代码中,while
循环取出每一轮的正则匹配,一共三轮。
ES2020 增加了 String.prototype.matchAll()
方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。
let reg = /t(e)(st(\d?))/g
let str = 'test1test2test3'
for (const match of str.matchAll(reg)) {
console.log(match)
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3", groups: undefined]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3", groups: undefined]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3", groups: undefined]
上面代码中,由于 str.matchAll(reg)
返回的是遍历器,所以可以用 for...of
循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。
遍历器转为数组是非常简单的,使用 ...
运算符和 Array.from()
方法就可以了
// 转为数组的方法一
[...str.matchAll(reg)]
// 转为数组的方法二
Array.from(str.matchAll(reg))