有很多种方法能实现数组滤重功能, 有人统计过在 JS 里至少就有 10 种方式.
本文关心的是: 能否用正则来实现滤重这个功能呢?
诚然, 就算能实现, 估计也没人会把它当成最佳实践的.
所以这里, 我们只考虑可能性.
本文给出的答案: 可以! 而且不止一种方式.
下面我们从易到难一步步来看如何实现的.
1. 相邻字符滤重问题
"abbccc" => "abc"
正则里要匹配之前出现过的字符, 需要使用反向引用:
- function distinct(string) {
- return string.replace(/(.)\1+/g, '$1')
- }
- console.log(distinct("abbccc"))
- // => "abc"
其中 \1 是反向引用, 指代第一个括号捕获的数据, 其中称为 (.) 为捕获分组. 而 $1 也表示第一个括号捕获的数据. 具体过程请看下图.
其中蓝色表示捕获分组捕获到的数据, 粉色的表示反向引用指代的数据. 进行替换操作后带颜色的数据只保留了蓝色数据.
2. 字符串滤重
"abbacbc" => "abc"
方式一
一般的字符串这么办呢?
最直接的思路是把问题转化为已解决过的问题.
把字符串拆分成数组, 然后字节码排序, 转化成相邻字符滤重问题.
这种方式, 用了数组相关方法, 正则的意味就没那么浓烈了.
方式二
使用循环, 删除重复出现的字符.
- function distinct(string){
- while(/(.).*?\1/.test(string)) {
- string = string.replace(/(.)(.*?)\1/, '$1$2')
- }
- return string;
- }
- console.log(distinct("abbacbc"))
- // => "abc"
用正则 /(.).*?\1/ 来判断字符串里是否还有重复字符, 有的话, 就替换一下. 替换的正则是 /(.)(.*?)\1/, 其中使用了两组括号, 为引用 $1 和 $2 提供了数据. 具体过程示图如下:
其中蓝色表示第一个捕获分组捕获的数据. 黑色表示第二组捕获分组捕获的信息, 粉色表示引用第一个捕获分组捕获的数据. 每一次替换, 粉色信息都被删除了.
方式三
方式二里使用了循环, 总觉得有点太笨. 其实可以直接使用 replace. 此时需要使用 (?=p):
- function distinct(string) {
- return string.replace(/(.)(?=.*?\1)/g, '')
- }
- console.log(distinct("abbacbc"))
- // => "abc"
具体过程示图如下:
(?=.*?\1) 表示匹配位置, 即图中绿色箭头所示. 如第一行中字符 a 后面的位置, 改位置后面的字符匹配 .*?\1, 其中 \1 即图中粉色的数据, 对应于第一个分组捕获的蓝色数据. 最后所有的蓝色数据都被替换成 '' 了.
这种实现方式有一个问题, 就是重复字符只保留最后出现的字符. 如果在原来字符串后面加个 "a" 变成 "abbacbca", 最终结果却是 "bca".
方式四
方式三的思路是看当前字符是否会在后面出现, 如果出现就删除. 方式四的逻辑却可以说反过来的: 如果当前字符在前面出现过, 那么就删除. 此时需要用断言 (?<=p), 看当前位置前面是否匹配 p.
正则不能想当然地写成 /(?<=.*?\1)(.)/g, 因为 \1 是 "反向" 引用, 只能引用它之前的分组. 所以这里要把它放在目标字符后面:
- function distinct(string) {
- return string.replace(/(.)(?<=\1.*?\1)/g, '')
- }
- console.log(distinct("abbacbc"))
- // => "abc"
具体过程如下:
比如图中第一行中第二个 b 后面的绿色箭头表示 (?<=\1.*?\1). 第一个 \1 是粉色 b, 第二个是蓝色的那个.
3. 数组滤重
有字符串滤重后, 数组滤重就简单了. 上面四种方法都可以写成数组版本的. 比如第四种方案如下:
- function distinct(arr) {
- return arr.join('').replace(/(.)(?<=\1.*?\1)/g,'').split('')
- }
- console.log(distinct(['a','b','b','a','c','b','c']))
- // => ['a', 'b', 'c']
至此我们的解决方案还有一些问题:
只能过滤数组的每个元素是一个字符的情形
过滤的结果会把元素转化为数字.
支持多位字符相对容易解决, 但是要保持类型的话, 需要 JSON 两个方法了.
最后给出方案四的最终版本:
- function distinct(arr) {
- var string = JSON.stringify(arr)
- string = string.replace(/,([^,]+)(?<=\1.*?\1)(?=,|])/g, (m, $1) => $1 == '"'? m :'')
- return JSON.parse(string)
- }
- console.log(distinct(["aa",1,"ab",true,1,true,"aa"]))
- // => ["aa", 1, "ab", true]
本文完.
另外, 欢迎阅读本人的《JS 正则迷你书》.
来源: https://juejin.im/post/5c2f0220e51d455023415955