正则表达式是用于匹配字符串中字符组合的模式。在 JavaScript中,正则表达式也是对象。这些模式被用于 RegExp 的 exec 和 test 方法, 以及 String 的 match、matchAll、replace、search 和 split 方法。
正则的意图
正则表达式是匹配模式,要么匹配字符串,要么匹配位置。
正则表达式是匹配模式,要么匹配字符串,要么匹配位置。
正则表达式是匹配模式,要么匹配字符串,要么匹配位置。
正则的创建
-
方式一:字面量的创建
const reg = // -
方式二:构造函数
const reg = new RegExp()
补充:
-
字面量创建是常用的方式。 -
字面量创建 和 构造函数的创建 是存在区别的
// eg: const num = 12.34 // 字面量 const reg1 = /\d+.\d+/ // 构造函数 const reg2 = new RegExp('\d+.\d+')reg1和reg2表达的意思是一样吗?答案是否定的。先看一段代码:
console.log('') // 报错 Javascript: unterminated string literal console.log('\d') // d在这里
\,在JS解析的时候是有特殊意义的。所以上面的构造函数方式的创建:
const reg = new RegExp('\d+.\d+') // d+.d+ 匹配的d 这个字符 // 需要被转义 const reg = new RegExp('\\d+\.\\d+')这种写法挺麻烦的,所以推荐使用 字面量的写法。
正则的原子
原子是正则表达式的最基本的组成单位,而且在每个模式中最少包含一个原子。
原子分为可见原子和不可见原子。
- 可见原子: Unicode编码表中键盘输入肉眼可见的字符。
- 不可见原子: Unicode编码表中键盘输入肉眼不可见的字符,如空格,制表符。
通用字符类型:
\d 匹配0-9任意一个数字 等价于:[0-9]
\D 除了数字以外的任意一个字符匹配 等价于:[^0-9]
\w 匹配数字、字母以及下划线 等价于:[a-zA-Z0-9]
\W 除了数字 字母 下划线以外的任意一个字符 等价于:[^a-zA-Z0-9]
\s 任意一个空白字符匹配
\S 任意一个空白字符意外的字符匹配[^\n\f\r\t\v]
\f 换页字符;
\n 换行字符;
\r 回车字符;
\t 制表符;
\v 垂直制表符;
正则的特殊字符
开始(^) 和 结束($)
对匹配的做开始的限定和结束的限定
const reg = /abc/ // abc aabc abcabc这些都是匹配成功
const reg = /^abc$/ // 匹配abc
转义字符(\)
\这个无论是在 正则还是在其他的地方都有着特殊的意义。
正则中有跟多的符号是有意义的,如果想要匹配他们都是需要转义的
\ ( ) [ ] ^ $ * + ? 这些符号都是有着自己各自的意义,下面会依次介绍
const str = '()[]^$*+?' // 这是一个字符串
const reg = /\(\)\[\]\^\$\*\+\?/ // 大量的使用转义
或(|)
左右两边看成一个整体,满足其中一个就行
const reg = /abc|acc/ // 匹配abc或者acc
点元字符(.)
匹配除\n之外的任何单个字符。
let str = `
james
kobe
`
console.log(str.match(/.+/)) // ' james'
. 基本上都匹配了所有。那么匹配全部该怎么写呢?
// 匹配所有
const reg = /[\d\D]+/
const reg = /[\s\S]+/
const reg = /.+/s // s模式
正则的量词(* + ?)
{m,n} : 匹配m到n个字符,都是数字,还可以写成{m,}: m到无限个
const reg = /d{2,4}/ // 匹配d的数量为2到4个
*: 0个到无穷个 等价于 {0,}
const reg = /d*/
+: 1个到无穷个 等价于 {1,}
const reg = /d+/
?: 1个或者0个
const reg = /d?/
注意: * + ? {m,n} 它们都是贪婪模式(就是尽可能的匹配多的),在下面的内容中会介绍。
原子表([])
[]满足其中的一个内容
const reg = /[abc]/ // a b c ab 都满足
区间匹配
[0-9] // 匹配0-9的数字
[a-z] // 匹配a-z的小写字母
[A-Z] // 匹配A-Z的大写字母
取反
[^abc] // 匹配除了abc以外的字符
这里 ^ 虽然跟开始字符是一样的,但是表示的意义不一样。这里的 ^ 是在 []使用。
不解析字符
[.()+] // 在原子表中,它们都是单纯的字符
原子组(())
() 与 原子表[]不同,()会把其中的内容看成一个整体。
const dom = `<h1>copyer</h1>`
const reg = /^<h([1-6])>[\s\S]*</h([1-6])>$/i
// 等价于
const reg1 = /^<h([1-6])>[\s\S]*</\1>$/
([1-6])看成一个整体,就是为了匹配 h1~h6的标签
在reg正则表达式中,匹配开始标签和结束标签,就会写两次的([1-6]),比较的重复性(但是推荐这么写,比较好理解);如果想写的高大尚,就使用组名吧。
1、组名
组名: 就是为了上面重复写的问题。
从左到右,依次找出原子组(),如果发现了重复的原子组,然后就看当前原子组排在前面几个原子组(不包括重复的)的第几个,就使用\num就行了;
比如: ([1-6]) 排在原子组的第一个,那么使用\1就可以了。
2、嵌套组
// 匹配url
const url = 'http://www.baidu.com'
const reg = /^https?://[\w]+\.[\w]+\.(com|cn)$/
console.log(url.match(reg))
match方法 后面会详细的解释到。
[
0:"http://www.baidu.com"
1: "com"
groups: undefined
index: 0
input: "http://www.baidu.com"
]
数组第一个,就是匹配到的字符串
如果有原子组的使用,匹配到的结果,就会依次放在第一个的后面
3、不记录组
上面不是对com 组提取出来了嘛,如果不想要记录该怎么做呢?
const reg = /^https?://[\w]+.[\w]+.(?:com|cn)$/
在原子组中: 使用?: 就不会记录在组中(放在原子组的最前面),使用match方法就不能匹配到相应的内容。
4、组的实际使用场景
给一个dom节点字符串,拿取里面的内容
let str = '<h1>copyer</h1>'
const reg = /<h([1-6])>(.*)<\/h([1-6])>/
console.log(str.match(reg)[2])
/* match方法的返回值:
[
'<h1>copyer</h1>',
'1',
'copyer',
'1',
index: 0,
input: '<h1>copyer</h1>',
groups: undefined
]*/
上面使用了三个原子组: ([1-6]) (.*) ([1-6])
这个三个都是push到数组中,内容就是第二个原子组,所以,我们取下标为 2,就可以拿到内容了
正则的贪婪模式
在上面,知道了正则的量词(* + ?) 的基本使用,但是呢,它们还是有一种特性: 贪婪
何为贪婪?
const str = 'aaaaaa'
const reg = /a+/
console.log(str.match(reg)) // aaaaaa
只要在条件的满足范围内,能匹配到多少就是多少;这就是贪婪思维
那么怎么阻止贪婪呢?
正则量词后面添加一个?
const str = 'aaaaaa'
const reg = /a+?/
console.log(str.match(reg)) // a
*? +? ?? {m,n}? 类似这种组合。
正则的属性lastIndex
lastIndex从字面理解:最后一个索引值。实际上,它的意思是正则表达式开始下一次查找的索引位置。
lastIndex 的默认值为0,当第一次查找到匹配的内容,完了之后会把lastIndex的值设为匹配到得字符串的最后一个字符的索引位置加1
const str = 'aaa_aa_aaaa'
const reg_g = /a+/g
console.log(reg_g.lastIndex) // 0 默认值
console.log(reg_g.exec(str)) // aaa
console.log(reg_g.lastIndex) // 3 上次匹配aaa 下标值为2,在上面的一次加 1,所以为3
console.log(reg_g.exec(str)) // 所以这次匹配是在 下标为3的位置开始匹配
如果没有匹配到内容的话,lastIndex 就会重新置为 0。
重要事项:不具有标志 g 和不表示全局模式的 RegExp 对象不能使用 lastIndex 属性。
但是后面,好像y修饰符也支持了。
lastIndex 是个 可读可写的属性。
正则的修饰符
s修饰符
/s 表示将字符串视为单行来匹配 。
const dom = `
<h1>123</h1>
`;
const reg = /.+/s;
console.log(dom.match(reg));
s模式,会把字符串看一行来解析
[
0: "\n <h1>123</h1>\n "
groups: undefined
index: 0
input: "\n <h1>123</h1>\n "
]
所以\n就解析出来了
i修饰符
正则是区分大小写的,如果是在i模式下,就不区分大小写了
g修饰符
全局匹配。
一般情况下,没有量词的情况下,匹配到一个,就不会继续匹配下去了。如果添加了 \g ,就会一直匹配下去满足条件的。
y修饰符
y 修饰符的作用与 g 修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。
不同之处:g 修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始(简单理解:y修饰符就是第一个位置就需要被满足,如果不满足,lastIndex就会被重置)
在这里,理解 y修饰符 和 g修饰符 需要理解上面的lastIndex 正则属性
示例:
const str = 'aaa_aa_aaaa'
const reg_g = /a+/g
const reg_y = /a+/y
console.log('第一次')
console.log(reg_g.lastIndex) // 0
console.log(reg_y.lastIndex) // 0
console.log(reg_g.exec(str)) // aaa
console.log(reg_y.exec(str)) // aaa
console.log('第二次')
console.log(reg_g.lastIndex) // 3
console.log(reg_y.lastIndex) // 3
console.log(reg_g.exec(str)) // aa
console.log(reg_y.exec(str)) // null
console.log('第三次')
console.log(reg_g.lastIndex) // 6
console.log(reg_y.lastIndex) // 0
console.log(reg_g.exec(str)) // aaaa
console.log(reg_y.exec(str)) // aaa
第一次, y修饰符 和 g修饰符是一样的, lastIndex 都从 0 变成了 3
第二次,g修饰符 可以继续匹配后面的剩余字符 aa ,所以lastIndex从 3 变成了 6
但是 y修饰符 需要的是剩余字符 的第一个需要满足,很显然第二次的剩余字符是不满足的,所以 匹配的值为 null,lastIndex 也从 重置了,变成 0
第三次,y修饰符有重新开始匹配。
u修饰符
不了解(只是知道有这个知识点),求指教。
正则的方法
两大类:字符串的方法(match、matchAll、replace、search、split) 和 正则的方法(test、exec)。
match()
语法:
str.match(regexp)
参数:
一个正则表达式对象。如果传入一个非正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp 。如果你没有给出任何参数并直接使用match() 方法 ,你将会得到一 个包含空字符串的 Array :[""] 。
返回值:
- 如果有
g修饰符,则返回所有匹配的信息,不会返回捕获值 - 如果没有
g修饰符,则返回一个完整以及相关的捕获值。
示例:
// 情况一: 存在g修饰符
const str = "copyer";
console.log(str.match(/c/g)); // ['c']
// 情况二: 不存在g修饰符
const str = "copyer";
console.log(str.match(/c/));
// 完整的信息
[
0: "c" // 捕获值
groups: undefined
index: 0
input: "copyer"
]
matchAll()
语法:
str.matchAll(regexp)
参数:
正则表达式对象。如果所传参数不是一个正则表达式对象,则会隐式地使用 new RegExp(obj) 将其转换为一个 RegExp。
RegExp必须是设置了全局模式g的形式,否则会抛出异常TypeError。
返回值:
一个迭代器
实例:
const str = "abaca";
const matchs = str.matchAll(/a/g) // 必须是全局模式
// 返回值是一个迭代对象,使用for...of遍历
for(let key of matchs) {
console.log(key)
}
// ['a', index: 0, input: 'abaca', groups: undefined]
// ['a', index: 2, input: 'abaca', groups: undefined]
// ['a', index: 4, input: 'abaca', groups: undefined]
这是获取所有捕获值的方法。但是在matchAll()出现之前,是通过正则方法 exec()来实现同样的目的。在后面的方法中会介绍到。
replace()
语法:
str.replace(regexp|substr, newSubStr|function)
参数:
-
regexp(pattern)一个
RegExp对象或者其字面量。该正则所匹配的内容会被第二个参数的返回值替换掉。 -
substr(pattern)一个将被
newSubStr替换的字符串。其被视为一整个字符串,而不是一个正则表达式。仅第一个匹配项会被替换。 -
newSubStr(replacement)用于替换掉第一个参数在原字符串中的匹配部分的
字符串。该字符串中可以内插一些特殊的变量名。 -
function(replacement)一个用来创建新子字符串的函数,该函数的返回值将替换掉第一个参数匹配到的结果。
返回值:
不改变原来的字符串,会返回一个新的字符串
实例:
// 情况一: 参数为字符串:参数看为一个整体,匹配到的第一项会被替换
const str = "abacab";
const newStr = str.replace('ab', 'f')
console.log(newStr) // facab
// 情况二: 参数为正则:匹配到的字符被新的字符串替换
const str = "abaca";
const newStr = str.replace(/a/g, 'f')
console.log(newStr) // fbfcf
// 情况三: 第二个参数为一个函数,返回的字符串用来替换
const str = "abaca";
const newStr = str.replace(/a/g, () => {
return '$#'
})
console.log(newStr) // $#b$#c$#
当然,这个函数不是表面上的这么简单,还有其他高深的用法。
search()
语法:
str.search(regexp)
参数:
一个正则表达式(regular expression)对象。如果传入一个非正则表达式对象 regexp,则会使用 new RegExp(regexp) 隐式地将其转换为正则表达式对象。
返回值:
如果匹配成功,则 search() 返回正则表达式在字符串中首次匹配项的索引;否则,返回 -1。
实例:
const str = "abaca";
console.log(str.search(/[b]/g)) // 1
console.log(str.search(/[d]/g)) // -1
split()
语法:
str.split([separator [, limit]])
参数:
-
separator指定表示每个拆分应发生的点的字符串。
separator可以是一个字符串或正则表达式 。 如果纯文本分隔符包含多个字符,则必须找到整个字符串来表示分割点。如果在str中省略或不出现分隔符,则返回的数组包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将str原字符串中每个字符的数组形式返回。 -
limit一个整数,限定返回的分割片段数量。当提供此参数时,split 方法会在指定分隔符的每次出现时分割该字符串,但在限制条目已放入数组时停止。如果在达到指定限制之前达到字符串的末尾,它可能仍然包含少于限制的条目。新数组中不返回剩下的文本。
返回值:
返回源字符串以分隔符出现位置分隔而成的一个 Array
实例:
const str = "abaca";
console.log(str.split('c')) // ['aba', 'a']
console.log(str.split('@')) // ['abaca']
console.log(str.split('')) // ['a', 'b', 'a', 'c', 'a']
console.log(str.split('a')) // ['', 'b', 'c', '']
console.log(str.split('', 3)) // ['a', 'b', 'a']
描述:
- 找到分隔符后,将其从字符串中删除,并将子字符串的数组返回。
- 如果没有找到或者省略了分隔符,则该数组包含一个由整个字符串组成的元素。
- 如果分隔符为空字符串,则将str转换为字符数组。
- 如果分隔符出现在字符串的开始位置、末尾位置或者同时存在,那么返回的数组将以空字符串开头、结尾。
注意:
如果分隔符是包含捕获括号的正则表达式,则每次分隔符匹配时,捕获括号的结果(包括任何未定义的结果)将被拼接到输出数组中。但是,并不是所有浏览器都支持此功能。
const str = "abaca";
console.log(str.split(/(a)/)) // ['', 'a', 'b', 'a', 'c', 'a', '']
test()
语法:
regexObj.test(str)
参数:
-
str用来与正则表达式匹配的字符串
返回值:
如果正则表达式与指定的字符串匹配 ,返回true;否则false。
实例:
let str = 'hello world!';
let result = /^hello/.test(str);
console.log(result); // true
exec()
语法:
regexObj.exec(str)
参数:
-
str要匹配正则表达式的字符串。
返回值:
如果匹配成功,exec() 方法返回一个数组(包含额外的属性 index 和 input ,参见下方表格),并更新正则表达式对象的 lastIndex属性。
完全匹配成功的文本将作为返回数组的第一项,从第二项起,后续每项都对应正则表达式内捕获括号里匹配成功的文本。
如果匹配失败,exec() 方法返回 null,并将 lastIndex 重置为 0 。
实例:
const str = "abaca";
const reg = /a/g
console.log(reg.lastIndex) // 0
console.log(reg.exec(str)) // ['a', index: 0, input: 'abaca', groups: undefined]
console.log(reg.lastIndex) // 1
lastIndex会被更新。
获取全部的匹配信息:
let str = "abaca";
let reg = /a/g;
let result;
while ((result = reg.exec(str)) != null) {
console.log(result)
}
// ['a', index: 0, input: 'abaca', groups: undefined]
// ['a', index: 2, input: 'abaca', groups: undefined]
// ['a', index: 4, input: 'abaca', groups: undefined]
错误写法:
let str = "abaca";
let reg = /a/g;
let result;
while (reg.exec(str) !== null) {
console.log(result)
}
警告: 不要把正则表达式字面量(或者
RegExp构造器)放在while条件表达式里。由于每次迭代时lastIndex的属性都被重置,如果匹配,将会造成一个死循环。并且要确保使用了'g'标记来进行全局的匹配,否则同样会造成死循环。
正则的断言
正先行断言(?=)
后面的内容,满足前面的条件。
实例:
我有一只铅笔,我有一只钢笔',把铅笔前面的一只,换成两只
let str = "我有一只铅笔,我有一只钢笔";
const reg = /一只(?=铅笔)/g
const newStr = str.replace(reg, '两只')
console.log(newStr) // 我有两只铅笔,我有一只钢笔
理解:
就看成是条件就行了,后面的内容 是否等于 '匹配的内容'。
在上面的例子中,想把一只改成两只,全局搜索的话,就会搜索出两个,我们只需要匹配铅笔的那一个,所以 判断条件是否等于(?=) 铅笔就行了
负先行断言(?!)
后面的内容,不满足前面的条件
实例:
let str = "我有一只铅笔,我有一只钢笔";
const reg = /一只(?!钢笔)/g
const newStr = str.replace(reg, '两只')
console.log(newStr) // 我有两只铅笔,我有一只钢笔
取反即可
正后发断言(?<=)
前面的内容,满足后面的条件
实例:
let str = "我有一个大爸和二爸";
const reg = /(?<=大)爸/g
const newStr = str.replace(reg, '爷')
console.log(newStr) // 我有一个大爷和二爸
把 大爸 和 二爸中的大爸的爸改成爷(只是举例,其他方式更加的简单)
负后发断言(?<!)
前面的内容,不满足后面的条件
实例:
let str = "我有一个大爸和二爸";
const reg = /(?<!二)爸/g
const newStr = str.replace(reg, '爷')
console.log(newStr) // 我有一个大爷和二爸
意思跟上面差不多
总结
断言:就看成条件
- 先行: 就是后面的内容需要满足的条件
/表达式(条件)/条件是写在后面的
- 后发: 就是前面的内容需要满足的条件
/(条件)/表达式条件是写在前面的
补充理解(回顾正则时,临时想到):
把字符串从右向左看:
先行:后面的内容先匹配(所以是匹配后面的内容)
后发:前面的内容后发现(所以是匹配前面的内容)
注意:
使用了小括号,但是不能看成是原子组
总结
花了两天的时间,从头学习了一遍正则的基本语法。当然,在实际实践中,还是需要积累的。
如果上面的内容有误,请提出来,多谢指教。