我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情。
大家好,我是
那个曾经的少年回来了
。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。
前言
最近在阅读axios中的工具函数的源码,说实话学到了很多知识,只要看不懂的我就不断的查阅资料,进行自我校验、自我巩固和自我讲解,总之就是不断的将自己不懂的基础知识挖深学习。因为之前查看源码的一段时间,保证的只是能看懂源码的逻辑就算是不错了,学习的深度还不够,没有真正的抓住源码的思想精髓。
阅读源码一部分是可以看到作者代码功底的深厚程度,看清作者如何通过一些基础融会贯通、想出一些奇思妙想,认清自己当前跟作者的差距,不断的夯实和优化自己,学习作者的思维习惯,不断的模仿优秀的开源大师,等到来年定能在一个小角落开花结果,所以稍安勿躁 切莫着急。
axios工具函数中,有两个方法使用了正则表达式来处理,痛定思痛。我下定决心想将这正则表达式蹂躏于脚下,让我从此不再惧怕看到正则,以前都是只会往上搜索然后直接使用,想看别人写的正则,真的门都没有。
但是经过这几天的摸索和学习我才发现,原来看上去那么高大上的正则也不过如此,掌握了一些规则和语法,便可以轻松的搞明白别人的正则想法。所以如果你也曾被正则困扰过,那么来看看我的学习思路,或许能给你带来一些自信,让你能有勇气去重新战胜它。 好了废话就不多说了,开始吧。
axios工具函数中有一个移除字符串首尾空格的方法,先判断是否包含trim方法,如果有直接调用trim方法进行移除,这是JavaScript自带的方法。 当然有些环境是不存trim方法的(比如IE8及以下版本是不支持trim方法的),就需要通过后面的正则进行去除首尾的空格以及特殊字符。
//去除空格的方法涉及正则
const trim = (str) => str.trim ?
str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
字符串方法 replace
的第一个参数 /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g
便是正则表达式。
// 将字符串转换为CameCase 驼峰式命名
const toCamelCase = str => {
return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g,
function replacer(m, p1, p2) {
return p1.toUpperCase() + p2;
}
);
};
同样的还有一个方法 将字符串转换为CameCase 驼峰式命名中也使用了正则/[_-\s]([a-z\d])(\w*)/g
。
其实现在来看这两个正则表达式,如果是像我一样之前没怎么接触过的话,肯定是一头雾水,什么玩意哟。那接下来我就通过我自己学习的方式,来慢慢的揭开正则表达式的神秘面纱。循序渐进,将所有的正则基础都做一个了解。
- ^ 相当于开头
- \s匹配空白字符(空格、制表符、其他空白)
\xA0
代表空格的意思。 在 IE 早期版本中,异步接口可能会拿到\uFEFF
开头的字符串,如果不 trim 掉,会导致 JSON.parse 失败。
1、从最简单的正则开始
1.1、匹配出单个字符
匹配字母a是否存在于字符串
let reg = /a/
reg.test('apple') // true
reg.test('pear') // true
reg.test('bed') // false
1.2、匹配出多个字符(或者叫字符串)
匹配字符串 world
是否存在于字符串
let reg = /world/
reg.test('hello world') // true
reg.test('good morning') // false
1.3、匹配某个字符串出现的次数
匹配 hello
字符串在 大字符串
中出现的次数
i
忽略字符串大小写g
全局匹配
let reg = /hello/ig
let str = 'hello world。 Hello, in a new world'
str.match(reg) //(2) ['hello', 'Hello']
1.4 替换字符串
let reg = /hello/ig
let str = 'hello world。 Hello, in a new world'
str.replace(reg, "aehyok") // 替换完输出 'aehyok world。 aehyok, in a new world'
i
忽略大小写替换字符串,所以会替换掉原来的hello
和Hello
g
全局范围内查找
2、匹配特殊字符
let reg = /\*/
reg.test('apple*') // true
reg.test('pear*') // true
reg.test('bed') // false
你可以去尝试一下,如果不加这个斜杠就会报错的
这里面的*
就算是一个特殊字符,就是这个字符其实在正则表达式中存在着特殊的语义。目前在正则中有特殊意义的字符有如下几个: ( [ { \ ^ $ | ) ] } ? * + .
。 另外这里还有一类特殊字符:
特殊字符 | 正则表达式 |
---|---|
空白符 | \s |
换行符 | \n |
回车符 | \r |
制表符 | \t |
垂直制表符 | \v |
换页符 | \f |
空白符包括: 空格、水平制表符、垂直制表符、换行符、回车符。
\s(小s)
包括 [\r\n\t\f\v ])。\S(大S)
是[^\r\n\t\f\v],非空白符。
使用的时候如下所示,只要有空格就可以匹配到,或者可以测试其他字符
let reg = /\n/
reg.test('apple\n*') // false
reg.test('apple*') // true
3、两种使用模式
字面量模式和字符串模式两种正则表达式的模式,最终达到的效果是一样的
3.1、字面量模式
判断是否以[aehyok]开头
// 字面量模式
let reg = /^\[aehyok\]/
reg.test('[aehyok]123') // true
reg.test('[aeh1k]123') // false
3.2、字符串模式
判断是否以[aehyok]开头
let reg = new RegExp('^\\[aehyok\\]')
reg.test('[aehyok]123') // true
reg.test('[aeh1k]123') // false
^
表示以什么开头的,专门用来匹配开头位置的,在匹配位置说明中,我也会进行实例讲解
4、字符匹配的次数
4.1、({m,n})连续出现最少m次,最多n次
let reg = /abc{2,4}d/g
reg.test("abcgg") // false 只匹配一个c
reg.test("abccgg") // true 能匹配两个c
reg.test("abccccgg") //true 能匹配四个c
reg.test("abcccccgg") ///false 超过四个c 没办法了
4.2、(?)匹配前面的子表达式出现0次或者1次,或者另外一种类型({0,1})
let reg = /abcde?f/
let reg = /abcde{0,1}f/
reg.test('abcdef') // true
reg.test('abcdf') // true
reg.test('abcdeef') // false
第一个出现了1次 第二个出现了0次 第三个出现了2次,这个就出问题了,为false
4.3、(+)匹配前面的子表达式1次或多次,或者另外一种类型({1,})
let reg = /[1-9][0-9]+/
let reg = /[1-9][0-9]{1,}/
reg.test("2") // false
reg.test("2a") // false
reg.test("21") // true
reg.test("212") // true
匹配后面的正则表达式[0-9] 一次或者多次,对于整个正则表达式来说,也可以说成是子表达式
4.4、(*)匹配前面的子表达式0次或多次,或者另外一种类型({0,})
let reg = /[1-9]*/
let reg = /[1-9]{0,}/
reg.test("") // true
reg.test("1") // true
reg.test("12") // true
4.5、变种({m,m})只出现过m次
m为数字
- 四位数字
let reg = /^[0-9]{4}$/
reg.test("1234") // true
reg.test("12s4") // false
reg.test("12345") // false
5、匹配位置说明
5.1、^ 开头位置的说明
// 以1234开头的字符串
let reg = /^1234/
reg.test('12345') // true
reg.test('123s5') // false
reg.test('123') // false
5.2、$ 结尾位置的说明
// 以1234结尾的字符串
let reg = /1234$/
reg.test('12345') // false
reg.test('111234') // true
5.3、(\b) 查看字符串中是否有单词开始abc
let reg = /\babc/
reg.test('hello abcdef') // true
reg.test('hello world zabcd') // false
reg.test('hello world zdefabc')
5.4、 (\b) 查看字符串中是否有单词以abc结尾的
let reg = /abc\b/
reg.test('hello abcdef') // false
reg.test('hello world zabcd') // false
reg.test('hello world zdefabc') // true
5.5、匹配单词中间部分
- (\B) 刚好是
\b
的反面
既不能匹配开头部分,也不能匹配结尾部分
let reg = /\Babc/
reg.test('hello abcdef') // false
reg.test('hello world zabcd') // true
reg.test('hello world zdefabc') // false
5.5、 (g和m)全局模式和多行模式
- 普通模式
let reg = /abc/
let str = "abcd abcdd \nabcd \nabcde"
str.match(reg) //['abc', index: 0, input: 'abcd abcdd \nabcd \nabcde', groups: undefined]
这样只会匹配出第一个
- (g)全局模式
let reg = /abc/g
let str = "abcd abcdd \nabcd \nabcde aabc"
str.match(reg) // ['abc', 'abc', 'abc', 'abc', 'abc']
全局模式下,会匹配到五个。不加全局的话,匹配到就结束匹配了,而加上全局模式,会将所有能匹配到的都匹配出来
- (m)多行模式
查看以abc开头,这里是一个字符串,并且我在中间加了两个\n
换行符。
let reg = /^abc/g //^表示位置,开始的位置
let str = "abcd abcdd \nabcd \nabcde aabc"
str.match(reg) // ['abc']
这样只会匹配到一个,只是一个字符串,并且以abc开头,没啥问题
现在我们再在上面的小例子中添加上m多行模式的匹配会是什么结果呢?
let reg = /^abc/gm //^表示位置,开始的位置
let str = "abcd abcdd \nabcd \nabcde aabc"
str.match(reg) // ['abc', 'abc', 'abc']
我加入\n
换行符,相当于一个多行的文本,这样去执行正则的时候相当于每一行都会去单独执行正则进行匹配。 这里相当于分成了三行进行匹配
- 组合模式
还是上面同一个字符串,现在假如我们想匹配出四个abc,四个以abc开头的单词,不是三个 也不是五个
let reg = /\babc/gm //^表示位置,开始的位置
let str = "abcd abcdd \nabcd \nabcde aabc"
str.match(reg) // ['abc', 'abc', 'abc', 'abc']
\b
单词的边界控制
m
开启多行模式匹配
5.6、(i)忽略大小写
let reg = /aBc/g
let str = "aBc aaBc aabc"
str.match(reg) // (2) ['aBc', 'aBc']
没有加i标识,也就是没有开启忽略大小写的时候
下面是加i标识的匹配,通过结果可以看出匹配到三个
let reg = /aBc/gi
let str = "aBc aaBc aabc"
str.match(reg) // (3) ['aBc', 'aBc', 'abc']
6、多个字符匹配
6.1、(\d) 表示一位数字
匹配到一位数字, 等同于[0-9]
let reg = /\d/
let reg = /[0-9]/
reg.test('1s') // true
reg.test('ss') // false
[]
表示匹配的字符在[]
中,并且只能出现一次,所以[0-9]
表示 从0到9出现任何一个数字即可匹配。
6.2、(\D)表示除数字以外的任意字符
匹配除数字意外的任意字符, 等同于[^0-9]
let reg = /\D/
let reg = /[^0-9]/
reg.test('1234') // false
reg.test('ss') // true
6.3、(.)除了换行符以外的任何字符
let reg = /./g
reg.test("\n") // false 只有换行符肯定为false
reg.test("1") // true 能匹配到不是换行符的便为true
6.4、(\w)匹配字母加数字加下划线_
let reg = /[0-9a-zA-z_]{4}/
reg.test("a1b_") // true
reg.test("a1=+") // false
简化模式如下
let reg = /\w{4}/
reg.test("a1b_") // true
reg.test("a1=+") // false
6.5、(\W)非单词字符下划线
匹配出四个字符
let reg = /\W{4}/
reg.test("=-+`") // true
reg.test("====") // true
reg.test("____") // false
6.6、(\s)匹配空白符和(\S)匹配非空白符
空白符包括: 空格、水平制表符、垂直制表符、换行符、回车符。
\s(小s)
包括 [\r\n\t\f\v ])。\S(大S)
是[^\r\n\t\f\v],非空白符。
特殊字符 | 正则表达式 |
---|---|
空白符 | \s |
换行符 | \n |
回车符 | \r |
制表符 | \t |
垂直制表符 | \v |
换页符 | \f |
let reg = /\s/
7、贪婪匹配和懒惰匹配
在正则表达式中,表示字符串重复个数的元字符 ?,+,*,{}
默认都会选择贪婪模式,会进行最大程度的匹配。 上面其实专门说过,这里再简单总结一下这几个 元字符
?
匹配前面的子表达式出现0次或者1次,或者另外一种类型({0,1})+
匹配前面的子表达式1次或多次,或者另外一种类型({1,})*
匹配前面的子表达式0次或多次,或者另外一种类型({0,}){m,n}
连续出现最少m次,最多n次
如果想切换到懒惰模式,就只需要在该元字符后面加多一个 ?
, 就代表切换到懒惰模式。 所以,贪婪模式就是尽可能多的匹配字符,而懒惰模式或者叫非贪婪模式,就是尽可能少的匹配字符。
我们来看一个简单的小例子, 从字符串 abczsssssabbbz
中匹配出axxz
也就是 a
与 z
之间至少有一个字符
let reg = /a.+z/
let str = 'abczsssssabbbz'
str.match(reg) // ['abczsssssabbbz', index: 0, input: 'abczsssssabbbz', groups: undefined]
通过结果可以发现,会匹配出整个字符串,这样可能不是我们想要的结果,因为通过最开头的几个字符可以看出已经可以匹配了abcz
,但这就是贪婪匹配匹配出尽可能多的字符,那我们想匹配出 abcz
,应该怎么处理呢?
let reg = /a.+?z/
let str = 'abczsssssabbbz'
str.match(reg) // ['abcz', index: 0, input: 'abczsssssabbbz', groups: undefined]
通过在量词符号 +
后添加一个 ?
就可以表示为懒惰匹配了。意思就代表尽可能少的匹配,那么在匹配 abczsssssabbbz
最开头的几个字符就满足要求了,就不会继续往后匹配了。
8、高阶用法
8.1、()子表达式
通过()
元字符所包含的正则表达式被分为一组,每个分组就是一个子表达式了。如果要发挥子表达式强大的作用,一般会结合回溯引用才会发挥其作用。
let reg = /(ab){1,3}(cd)/g
let str = 'abcd ababcdef abababcd ababababcd acd aacd bbcd'
str.match(reg) // ['abcd', 'ababcd', 'abababcd', 'abababcd']
上面的正则 (ab){1,3}(cd)
便是(ab)
作为第一个 分组
或者 子表达式
出现至少1次,最多3次,然后后面还有一个分组 (cd)
后没有次数,那么匹配一次就可以了。所以最终的结果就是匹配成功四组,第四组可能很多人奇怪,其实是正常的,因为第四组字符串 ababababcd
ab重复四次,从这个字符串中可以找到匹配了三次 ab
然后再加上 cd
,也就是从这个字符串第三个位置开始匹配就能匹配成功了。
那么现在假如我们只想匹配前三个怎么办呢,其实很简单
let reg = /\b(ab){1,3}(cd)/g
let str = 'abcd ababcdef abababcd ababababcd acd aacd bbcd'
str.match(reg) // ['abcd', 'ababcd', 'abababcd']
我只是在正则最前面加上了 \b
,相信看过前面的可能都还记得 \b
就是匹配单词开头部分的,只想匹配单词非开头或者非结尾,也就是单词中间部分的话可以使用 \B
,如果忘记了,可以回看番看一下。
8.2、回溯引用
回溯引用指的是模式的后面部分引用前面已经匹配到的子字符串。这里可以把它当做变量,类似的用法就像是 \1
、 \2
以此类推。\0
代表整个正则表达式。
let reg = /\b(\w+)\s\1/g
let str = 'Hello the new new world , I I am is a big big old'
str.match(reg) // (3) ['new new', 'I I', 'big big']
\b
所在位置可以匹配单词开头()
代表一个子表达式,其中是\w+
匹配字母加数字加下划线_
,+
表示匹配一次或多次\s
匹配空白字符(空格、制表符、其他空白)\1
引用前面表达式\b(\w+)\s
已经匹配的字符串,再次匹配 也就是匹配两次
9、总结
正则表达式在日常的开发中还是经常使用到的,但是大部分的时间都是在网上查找现成的来使用,有时候想稍微调整一下都非常困难,因为根本不了解正则表达式的一些基础知识。
通过最近的学习我发现入门正则表达式还是非常简单的,就是把一些基础的正则写出来,然后根据字符串去校验一下,再通过一些实际的例子去巩固自己的正则基础。当然目前我对正则一些高阶的用法理解的还不够透彻,还需要一段时间的消化和吸收。
不过现在说真的看别人写过的正则最起码能看懂,而且也能简单的进行修改,还是非常有成就感的。
所以文章开头第一个正则去除字符串前后的空格还是非常好理解的。而两个方法都使用了替换函数 String.prototype.replace()
str.replace(regexp|substr, newSubStr|function)
先举个最简单的例子
let reg = /o/
let str = 'hello world'
let newStr = str.replace(reg, 'zz')
console.log(newStr) // hellzz world
这里匹配替换掉了第一个o
为 zz
,如果想将 world
中的 o
也进行替换稍做调整即可
let reg = /o/g
let str = 'hello world'
let newStr = str.replace(reg, 'zz')
console.log(newStr) // hellzz wzzrld
上面会匹配替换第一个即停止替换了,而加上 g
进行全局匹配替换
再来说一下 replace
方法第二个参数为函数的情况,匹配成功后,函数的返回值就会作为替换字符串。
let reg = /o/g
let str = 'hello world'
let newStr = str.replace(reg,
function replacer(m, p1) {
console.log(m, p1); // o 4 // o 7
return 'zzz';
})
console.log(newStr) // hellzz wzzrld
const toCamelCase = str => {
return str.toLowerCase().replace(/[_-\s]([a-z\d])(\w*)/g,
function replacer(m, p1, p2) {
console.log(m, p1, p2)
return p1.toUpperCase() + p2;
}
);
};
const str = toCamelCase("_HelloWorld")
console.log(str) //Helloworld
这里其实默认只匹配一个单词的,然后将首字母大写,剩余的首字母后全部小写
- 那么这里的
p1
相当于第一个字表达式([a-z\d])
匹配的 p2
就当于第二个子表达式(\w*)
中匹配的
P1匹配出首字母以后,通过 p1.toUpperCase()
将首字母大写,然后 P2
匹配出剩余字母保持全小写的状态,我这里故意用了两个单词,但是对于程序来说,它只会作为一个单词来考虑。
这里除了 String.prototype.replace()
替换函数外,还有三个,有兴趣的可以去mdn详细学习
String.prototype.match()
: 匹配字符串,返回匹配的数组或nullString.prototype.test()
: 监测字符串的正则匹配,返回匹配的数组或nullString.prototype.search()
: 查找指定字符串,返回第一个匹配的起始位置
我的个人博客:http://vue.tuokecat.com/blog
我的个人github:https://github.com/aehyok
我的前端项目:pnpm + monorepo + qiankun + vue3 + vite3 + 工具库、组件库 + 工程化 + 自动化
不断完善中,整体框架都有了
在线预览:http://vue.tuokecat.com
github源码:https://github.com/aehyok/vue-qiankun