零宽断言正则表达式替换方案

1、零宽断言正则是什么、有什么用

零宽断言是正则表达式中的一种方法,断言用来声明一个应该为真的事实,正则表达式中只有当断言为真时才会继续进行匹配。它和一般正则的作用区别是:它是主要用来取值的(如:一个字符串中,返回子字符串后面是$符号的子字符串)。如果不用取值,用一般的正则即可。

2、零宽断言有哪些类型

零宽正向先行断言(也叫正预测先行断言):(?=exp),它断言自身出现的位置的后面能匹配表达式exp

举个栗子:

const reg = /[a-z]+(?=\$)/g;
const str = 'a$bc$1$dd';
str.match(reg);
// 输出:['a', 'bc']

正则表达式的意思是:匹配后面跟着$符号的连续小写字母。

匹配时,a后面紧跟$,符合条件,继续匹配;bc后面紧跟$,符合条件,继续匹配;1为非小写字母,不符合条件,跳过;dd后面没有$,不符合条件,跳过。匹配完毕。所以最终的结果是['a', 'bc']

零宽正向后发断言(也叫正回顾后发断言):(?<=exp),它断言自身出现的位置的前面能匹配表达式exp

举个栗子:

const reg = /(?<=\$)[a-z]+/g;
const str = 'a$bc$1$dd';
str.match(reg);
// 输出:['bc', 'dd']

正则表达式的意思是:匹配前面是$符号的连续小写字母。

匹配时,从0位置开始,a前面没有$,匹配失败,跳过;$前面没有$,匹配失败,跳过;b前面是$,符合条件,又因为[a-z]+,会将bc作为一个整体匹配成功;同理最后匹配上dd

零宽负向先行断言(也叫负预测先行断言):(?!exp),它断言自身位置的后面不能匹配表达式exp

举个栗子:

const reg = /[a-z]+(?!\$)/g;
const str = 'a$bc$1$dd';
str.match(reg);
// 输出: ['b', 'dd']

正则表达式的意思是:匹配后面不是$符号的连续小写字母

零宽负向后发断言(也叫负回顾后发断言):(?

举个栗子:

const reg = /(?

正则表达式的意思是:匹配前面不是$符号的连续小写字母

注意:书写位置很重要,先行断言一般放在要获取的子字符串之后,后发断言放在要获取的子字符串之前。如果书写错误,则会造成意料之外的结果。如:

const reg0 = /(?=12)[a-z]+/g;
const str0 = '12abc';
str0.match(reg0);
// 输出:null

// =======================

const reg1 = /(?=12)[a-z0-9]+/g;
const str1 = '12abc';
str1.match(reg1);
// 输出:['12abc']

// ========================

const reg2 = /(?=12)12+/g;
const str2 = '12';
str2.match(reg2);
// 输出:['12']

// =========================

const reg3 = /(?=12)123/g;
const str3 = '12';
str3.match(reg3);
// 输出:null

是不是有点奇怪?reg0的零宽看起来像是正向先行断言,但书写的位置又是放在了前面(正常应该是在后面)。研究了一番,发现了一点规律:即后面的子字符串范围一定是要包含前面的断言。比如:reg1表达的是:查找以12开头的仅包含小写字母和数字的子字符串;reg2表达的是:查找以12开头的后面是1个(包含了12当中的2)或多个2的数字子字符串。总结:位置不书写正确的零宽正则,意义不大。

3、为什么要替换零宽断言正则

在safari浏览器中,无法识别零宽断言正则表达式

4、零宽断言正则的替换方案

零宽断言正则的优势是可以获取不包含断言部分的子字符串,直接获取到我们想要的目标字符。

以下,我们分两种情况讨论零宽(以reg=/(?<=\$)[a-z]+(?=\$)/, str='$a$bc$dd$'为例)的替换方案

4.1、对于不含g的零宽正则

reg=/(?<=\$)[a-z]+(?=\$)/

输出:str.match(reg)[0]

结果: a

这种情况简单很多,首先将零宽断言部分去掉断言符号仅留下断言内后面的普通字符变成\$/[a-z]+\$/,其次,对要匹配的字符进行分组/\$([a-z]+)\$/,最后用match获取结果,结果的第1个分组即为目标字符。

const reg = /\$([a-z]+)\$/;
const str = '$a$bc$dd$';
const result = str.match(reg);
console.log(result);
// 输出:['$a$', 'a', index: 0, input: '$a$bc$dd$', groups: undefined]
console.log(result?.[1])
// 输出:a

4.1、对于含g的零宽正则

reg=/(?<=\$)[a-z]+(?=\$)/g

输出:str.match(reg)

结果:['a', 'bc', 'dd']

4.1.1、如果不要求兼容IE,可以使用matchAll来实现

const reg = /\$([a-z]+)\$/g;
const str = '$a$bc$dd$';
const result = str.matchAll(reg);
console.log([...result]);
// 输出:
// [
//   ['$a$', 'a', index: 0, input: 'a$bc$1$dd', groups: undefined],
//   ['$dd$', 'dd', index: 5, input: '$a$bc$dd$', groups: undefined]
// ]
console.log([...result].map(r => r[1]))
// 输出:['a', 'dd']

可以看到,matchAll也输出了目标字符组成的数组。但matchAll的输出结果与零宽结果有一点小差异,零宽结果为长度为3的数组,matchAll结果为长度为2的数组,少了一个bc。这里的差异暂且按下不表,在4.1.2的实现一中进行解释。

4.1.2、如果要求兼容IE,则不可以使用诸如matchAll、replaceAll等es11引入的新特性,换成match或者replace采用二次获取的方法获取结果。

实现一:zeroWidthRegPolyfill

/**
 * n为正则分组的第几个$n,reg一般均为带g正则,否则等同str.match(reg)
 *  */
const zeroWidthRegPolyfill = (str: string, reg: RegExp, n: number = 1) => {
  let result = null;
  const originRegStr = reg.toString();
  const regStr = originRegStr.replace(/^\/(.*)+?(\/|\/g)$/, '$1');
  const regWithoutG = new RegExp(regStr);
  if (originRegStr.endsWith('g')) {
    const arr = str.match(reg);
    if (!arr) {
      return result;
    }
    result = [];
    arr.forEach((it: any) => {
      result.push(it?.match(regWithoutG)?.[n]);
    });
  } else {
    result = str.match(regWithoutG);
  }
  return result;
};

const reg0 = /\$([a-z]+)\$/g;
const str0 = '$a$bc$dd$';
zeroWidthRegPolyfill(str0, reg0);
// 输出:['a', 'dd']

可以看到,zeroWidthRegPolyfill的实现和matchAll一样,输出结果比使用零宽时,少输出了bc。这是因为“零宽正则”匹配完了之后,会从匹配到的字符开始继续匹配,预查不消耗字符

一般场景,其实是需要消耗字符的。但是,如果需要完全与零宽正则相匹配,则需要使用升级版方法zeroWidthRegPolyfillPlus 。

实现二:zeroWidthRegPolyfillPlus

const zeroWidthRegPolyfillPlus = (str, reg, n = 1) => {
  let result = null;
  const originRegStr = reg.toString();
  const regStr = originRegStr.replace(/^\/(.*)+?(\/|\/g)$/, '$1');
  const regWithoutG = new RegExp(regStr);
  let hasMatch = true;
  // 实现零宽预查补偿消耗字符,循环匹配
  // const loopReg = (_str, res) => {
  //   hasMatch = false;
  //   const nextStr = _str.replace(regWithoutG, function () {
  //     hasMatch = true;
  //     res.push(arguments[n]);
  //     return arguments[n + 1];
  //   });
  //   if (hasMatch) {
  //     loopReg(nextStr, res);
  //   }
  // };
  if (originRegStr.endsWith('g')) {
    result = [];
    // 实现补偿方法一:
    // loopReg(str, result);
    // 实现补偿方法二:
    let nextStr = str;
    while (hasMatch) {
      hasMatch = false;
      nextStr = nextStr.replace(regWithoutG, function () {
        hasMatch = true;
        result.push(arguments[n]);
        return arguments[n + 1];
      });
    }
  } else {
    result = str.match(regWithoutG);
  }
  return result;
};
 
// 目标:查找$$之间包裹的所有字符串
const str = '$a$bc$dd$';
// 1、零宽实现
const regZero = /(?<=\$)[a-z]+(?=\$)/g;
let result = str.match(regZero);
console.log('result1:', result);
// 返回 result = ['a', 'bc', 'dd']
// 此处需要将正向肯定预查进行分组,以便方法中能够将预查的字符消耗重新补上
const reg = /\$([a-z]+)(\$)/g;
 
result = zeroWidthRegPolyfillPlus(str, reg);
console.log('result2:', result);
// 返回 result = ['a', 'bc', 'dd']

注意:正则改写成/\$(.*?)(\$)/g;正向预查字符需包裹,以便方法中能够将预查的字符消耗重新补上

5、总结

零宽正则表达式可以利用matchAll新特性替代,也可以使用match、replace进行二次匹配,获取目标字符。其核心都是利用了正则表达式的分组功能。

以上就是本次讨论零宽断言替换方案的所有内容!感谢您的阅读

你可能感兴趣的:(前端学习笔记,正则表达式,javascript,开发语言)