Skip to content

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

大家好,我是 那个曾经的少年回来了。10年前我也曾经年轻过,如今已步入被淘汰的年龄,但现在幡然醒悟,所以活在当下,每天努力一点点,来看看2024年的时候自己会是什么样子吧,2024年的前端又会是什么样子,而2024年的中国乃至全球又会变成什么样子,如果你也有想法,那还不赶紧行动起来。期待是美好的,但是更重要的是要为美好而为之奋斗付诸于行动。

前言

最近在阅读axios中的工具函数的源码,说实话学到了很多知识,只要看不懂的我就不断的查阅资料,进行自我校验、自我巩固和自我讲解,总之就是不断的将自己不懂的基础知识挖深学习。因为之前查看源码的一段时间,保证的只是能看懂源码的逻辑就算是不错了,学习的深度还不够,没有真正的抓住源码的思想精髓。

阅读源码一部分是可以看到作者代码功底的深厚程度,看清作者如何通过一些基础融会贯通、想出一些奇思妙想,认清自己当前跟作者的差距,不断的夯实和优化自己,学习作者的思维习惯,不断的模仿优秀的开源大师,等到来年定能在一个小角落开花结果,所以稍安勿躁 切莫着急。

axios工具函数中,有两个方法使用了正则表达式来处理,痛定思痛。我下定决心想将这正则表达式蹂躏于脚下,让我从此不再惧怕看到正则,以前都是只会往上搜索然后直接使用,想看别人写的正则,真的门都没有。

但是经过这几天的摸索和学习我才发现,原来看上去那么高大上的正则也不过如此,掌握了一些规则和语法,便可以轻松的搞明白别人的正则想法。所以如果你也曾被正则困扰过,那么来看看我的学习思路,或许能给你带来一些自信,让你能有勇气去重新战胜它。 好了废话就不多说了,开始吧。

axios工具函数中有一个移除字符串首尾空格的方法,先判断是否包含trim方法,如果有直接调用trim方法进行移除,这是JavaScript自带的方法。 当然有些环境是不存trim方法的(比如IE8及以下版本是不支持trim方法的),就需要通过后面的正则进行去除首尾的空格以及特殊字符。

javascript
//去除空格的方法涉及正则
const trim = (str) => str.trim ?
  str.trim() : str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');

字符串方法 replace的第一个参数 /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g便是正则表达式。

javascript
// 将字符串转换为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是否存在于字符串

javascript
let reg = /a/
reg.test('apple')   // true
reg.test('pear')  // true
reg.test('bed')   // false

1.2、匹配出多个字符(或者叫字符串)

匹配字符串 world是否存在于字符串

javascript
let reg = /world/
reg.test('hello world')   // true
reg.test('good morning')  // false

1.3、匹配某个字符串出现的次数

匹配 hello 字符串在 大字符串中出现的次数

  • i 忽略字符串大小写
  • g 全局匹配
javascript
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 忽略大小写替换字符串,所以会替换掉原来的 helloHello
  • g 全局范围内查找

2、匹配特殊字符

javascript
let reg = /\*/
reg.test('apple*')   // true
reg.test('pear*')  // true
reg.test('bed')   // false

你可以去尝试一下,如果不加这个斜杠就会报错的

image.png

这里面的*就算是一个特殊字符,就是这个字符其实在正则表达式中存在着特殊的语义。目前在正则中有特殊意义的字符有如下几个: ( [ { \ ^ $ | ) ] } ? * + . 。 另外这里还有一类特殊字符:

特殊字符正则表达式
空白符\s
换行符\n
回车符\r
制表符\t
垂直制表符\v
换页符\f

空白符包括: 空格、水平制表符、垂直制表符、换行符、回车符。 \s(小s) 包括 [\r\n\t\f\v ])。\S(大S) 是[^\r\n\t\f\v],非空白符。

使用的时候如下所示,只要有空格就可以匹配到,或者可以测试其他字符

javascript
let reg = /\n/
reg.test('apple\n*')   // false
reg.test('apple*')   // true

3、两种使用模式

字面量模式和字符串模式两种正则表达式的模式,最终达到的效果是一样的

3.1、字面量模式

判断是否以[aehyok]开头

javascript
// 字面量模式
let reg = /^\[aehyok\]/
reg.test('[aehyok]123')  // true
reg.test('[aeh1k]123')  // false

3.2、字符串模式

判断是否以[aehyok]开头

javascript
let reg = new RegExp('^\\[aehyok\\]')
reg.test('[aehyok]123')  // true
reg.test('[aeh1k]123')  // false
  • ^表示以什么开头的,专门用来匹配开头位置的,在匹配位置说明中,我也会进行实例讲解

4、字符匹配的次数

4.1、({m,n})连续出现最少m次,最多n次

javascript
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})

javascript
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,})

javascript
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,})

javascript
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为数字

  • 四位数字
javascript
let reg = /^[0-9]{4}$/
reg.test("1234")  // true
reg.test("12s4")  // false
reg.test("12345") // false

5、匹配位置说明

5.1、^ 开头位置的说明

javascript
// 以1234开头的字符串
let reg = /^1234/

reg.test('12345') // true
reg.test('123s5') // false
reg.test('123')  // false

5.2、$ 结尾位置的说明

javascript
// 以1234结尾的字符串
let reg = /1234$/

reg.test('12345') // false
reg.test('111234') // true

5.3、(\b) 查看字符串中是否有单词开始abc

javascript
let reg = /\babc/
reg.test('hello abcdef')    // true
reg.test('hello world zabcd') // false
reg.test('hello world zdefabc')

5.4、 (\b) 查看字符串中是否有单词以abc结尾的

javascript
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 的反面

既不能匹配开头部分,也不能匹配结尾部分

javascript
let reg = /\Babc/
reg.test('hello abcdef')     // false
reg.test('hello world zabcd')  // true
reg.test('hello world zdefabc')   // false

5.5、 (g和m)全局模式和多行模式

  • 普通模式
javascript
let reg = /abc/
let str = "abcd abcdd   \nabcd \nabcde" 
str.match(reg)   //['abc', index: 0, input: 'abcd abcdd   \nabcd \nabcde', groups: undefined]

这样只会匹配出第一个

  • (g)全局模式
javascript
let reg = /abc/g
let str = "abcd abcdd   \nabcd \nabcde aabc" 
str.match(reg)   // ['abc', 'abc', 'abc', 'abc', 'abc']

全局模式下,会匹配到五个。不加全局的话,匹配到就结束匹配了,而加上全局模式,会将所有能匹配到的都匹配出来

  • (m)多行模式

查看以abc开头,这里是一个字符串,并且我在中间加了两个\n 换行符。

javascript
let reg = /^abc/g   //^表示位置,开始的位置
let str = "abcd abcdd   \nabcd \nabcde aabc" 
str.match(reg)   // ['abc']

这样只会匹配到一个,只是一个字符串,并且以abc开头,没啥问题

现在我们再在上面的小例子中添加上m多行模式的匹配会是什么结果呢?

javascript
let reg = /^abc/gm   //^表示位置,开始的位置
let str = "abcd abcdd   \nabcd \nabcde aabc" 
str.match(reg)   //  ['abc', 'abc', 'abc']

我加入\n 换行符,相当于一个多行的文本,这样去执行正则的时候相当于每一行都会去单独执行正则进行匹配。 这里相当于分成了三行进行匹配

  • 组合模式

还是上面同一个字符串,现在假如我们想匹配出四个abc,四个以abc开头的单词,不是三个 也不是五个

javascript
let reg = /\babc/gm   //^表示位置,开始的位置
let str = "abcd abcdd   \nabcd \nabcde aabc" 
str.match(reg)   //  ['abc', 'abc', 'abc', 'abc']

\b单词的边界控制

m 开启多行模式匹配

5.6、(i)忽略大小写

javascript
let reg = /aBc/g
let str = "aBc  aaBc aabc"
str.match(reg) // (2) ['aBc', 'aBc']

没有加i标识,也就是没有开启忽略大小写的时候

下面是加i标识的匹配,通过结果可以看出匹配到三个

javascript
let reg = /aBc/gi
let str = "aBc  aaBc aabc"
str.match(reg) // (3) ['aBc', 'aBc', 'abc']

6、多个字符匹配

6.1、(\d) 表示一位数字

匹配到一位数字, 等同于[0-9]

javascript
let reg = /\d/
let reg = /[0-9]/
reg.test('1s')  // true
reg.test('ss')  // false

[] 表示匹配的字符在 [] 中,并且只能出现一次,所以 [0-9] 表示 从0到9出现任何一个数字即可匹配。

6.2、(\D)表示除数字以外的任意字符

匹配除数字意外的任意字符, 等同于[^0-9]

javascript
let reg = /\D/
let reg = /[^0-9]/
reg.test('1234')  // false
reg.test('ss')  // true

6.3、(.)除了换行符以外的任何字符

javascript
let reg = /./g
reg.test("\n")  // false 只有换行符肯定为false
reg.test("1")   // true 能匹配到不是换行符的便为true

6.4、(\w)匹配字母加数字加下划线_

javascript

let reg = /[0-9a-zA-z_]{4}/
reg.test("a1b_")  // true
reg.test("a1=+")  // false

简化模式如下

javascript
let reg = /\w{4}/
reg.test("a1b_")  // true
reg.test("a1=+")  // false

6.5、(\W)非单词字符下划线

匹配出四个字符

javascript
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
javascript
let reg = /\s/

7、贪婪匹配和懒惰匹配

在正则表达式中,表示字符串重复个数的元字符 ?,+,*,{} 默认都会选择贪婪模式,会进行最大程度的匹配。 上面其实专门说过,这里再简单总结一下这几个 元字符

  • ? 匹配前面的子表达式出现0次或者1次,或者另外一种类型({0,1})

  • + 匹配前面的子表达式1次或多次,或者另外一种类型({1,})

  • * 匹配前面的子表达式0次或多次,或者另外一种类型({0,})

  • {m,n} 连续出现最少m次,最多n次

如果想切换到懒惰模式,就只需要在该元字符后面加多一个 ?, 就代表切换到懒惰模式。 所以,贪婪模式就是尽可能多的匹配字符,而懒惰模式或者叫非贪婪模式,就是尽可能少的匹配字符。

我们来看一个简单的小例子, 从字符串 abczsssssabbbz 中匹配出axxz也就是 az 之间至少有一个字符

javascript
let reg = /a.+z/
let str = 'abczsssssabbbz'
str.match(reg)   // ['abczsssssabbbz', index: 0, input: 'abczsssssabbbz', groups: undefined]

通过结果可以发现,会匹配出整个字符串,这样可能不是我们想要的结果,因为通过最开头的几个字符可以看出已经可以匹配了abcz,但这就是贪婪匹配匹配出尽可能多的字符,那我们想匹配出 abcz,应该怎么处理呢?

javascript
let reg = /a.+?z/
let str = 'abczsssssabbbz'
str.match(reg)   // ['abcz', index: 0, input: 'abczsssssabbbz', groups: undefined]

通过在量词符号 + 后添加一个 ? 就可以表示为懒惰匹配了。意思就代表尽可能少的匹配,那么在匹配 abczsssssabbbz 最开头的几个字符就满足要求了,就不会继续往后匹配了。

8、高阶用法

8.1、()子表达式

通过() 元字符所包含的正则表达式被分为一组,每个分组就是一个子表达式了。如果要发挥子表达式强大的作用,一般会结合回溯引用才会发挥其作用。

javascript
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,也就是从这个字符串第三个位置开始匹配就能匹配成功了。

那么现在假如我们只想匹配前三个怎么办呢,其实很简单

javascript
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代表整个正则表达式。

javascript
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()

javascript
str.replace(regexp|substr, newSubStr|function)

先举个最简单的例子

javascript
let reg = /o/
let str = 'hello world'
let newStr = str.replace(reg, 'zz')
console.log(newStr) // hellzz world

这里匹配替换掉了第一个ozz,如果想将 world中的 o也进行替换稍做调整即可

javascript
let reg = /o/g
let str = 'hello world'
let newStr = str.replace(reg, 'zz')
console.log(newStr) // hellzz wzzrld

上面会匹配替换第一个即停止替换了,而加上 g 进行全局匹配替换

再来说一下 replace 方法第二个参数为函数的情况,匹配成功后,函数的返回值就会作为替换字符串。

javascript
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

image.png

javascript
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() : 匹配字符串,返回匹配的数组或null
  • String.prototype.test() : 监测字符串的正则匹配,返回匹配的数组或null
  • String.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