正则表达式中的模板文字

Template literal inside of the RegEx

我试图在 RegEx 中放置一个模板文字,但没有成功。然后我创建了一个变量 regex 来保存我的正则表达式,但它仍然没有给我想要的结果。

但是,如果我单独 console.log(regex),我会收到所需的正则表达式,例如 /.+?(?=location)/i/.+?(?=date)/i 等等,但是一旦我将 regex 放在.replace 它似乎不起作用

function validate (data) {
  let testArr = Object.keys(data);
  errorMessages.forEach((elem, i) => {
    const regex = `/.+?(?=${elem.value})/i`;
    const a = testArr[i].replace(regex, '');
    })
  }

您的 regex 变量是一个 String。要使其成为 RegExp,请使用 RegExp 构造函数:

const regex = new RegExp(String.raw`pattern_as_in_regex_literal_without_delimiters`)

例如,像 /<\d+>/g 这样的正则表达式文字可以重写为

const re = RegExp(String.raw`<\d+>`, 'g') // One \ is a literal backslash
const re = RegExp(`<\d+>`, 'g')       // Two \ are required in a non-raw string literal

要插入变量,您可以使用

const digits = String.raw`\d+`;
const re = RegExp(`<${digits}>`, 'g')

要解决您的问题,您可以使用

const regex = new RegExp(`.+?(?=${elemvalue.replace(/[-\/\^$*+?.()|[\]{}]/g, '\$&')})`, "i"); 

另外,escape the variable 加入正则表达式是个好主意,这样所有特殊的正则表达式元字符都被视为文字。

const s = "final (location)";
const elemvalue = "(location)";
const regex = new RegExp(`.+?(?=${elemvalue.replace(/[-\/\^$*+?.()|[\]{}]/g, '\$&')})`, "i");
// console.log(regex); // /.+?(?=\(location\))/i
// console.log(typeof(regex)); // object
let a = s.replace(regex, '');
console.log(a);

A more advanced form of template literals are Tagged templates. - MDN

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");

const regex = ({ // (1)
  raw: [part, ...parts]
}, ...subs) => new RegExp(
  subs.reduce( // (2)
    (result, sub, i) => `${result}${escape(sub)}${parts[i]}`,
    part
  )
);


const t1 = `d`;
const r1 = regex `^ab(c${t1}e)`;  // (3) 
// /^ab(cde)/
console.log('r2.test(`abc${t1}e`); ➜', r1.test(`abc${t1}e`)); // true

// Check for proper escaped special chars
const t2 = `(:?bar)\d{2}`;
const r2 = regex `foo${t2}`; 
// /foo\(:\?bar\)d\{2\}/  ➜ t2 is escaped!

console.log('r2.test(`foo${t2}`); ➜', r2.test(`foo${t2}`)); // true
console.log('r2.test(`foo\(:\?bar\)d\{2\}`); ➜', r2.test(`foo\(:\?bar\)d\{2\}`)); // true
console.log('r2.test(`foobar11`); ➜', r2.test(`foobar11`)); // false
console.log(r2);

工作原理:

  1. 定义一个“标签函数”(名称无关紧要)例如regex() 模板文字字符串 .

    构建我们的正则表达式 (return new RegExp())

    Hint: Tag functions don't even need to return a string!

    • 第一个参数parts是一个string[](注入变量之间的段)。

      a${..}bc${..}de['a', 'bc', 'de'] = parts

      Hint: The special raw property, available on the first argument to the tag function, allows you to access the raw strings as they were entered, without processing escape sequences.

      这就是我们需要的!我们可以解构first参数,拆分出parts的第一个part。这是我们的第一个 regExp result 段。

    • subs...substitution 来自注入变量的值:

      ..${'x'}..${'y'}..['x', 'y']

  2. ...subs 上循环 并连接正则表达式 string:

    1. escape() 来自当前 stubs[i] 的特殊字符(这会破坏最终表达式)。
    2. 附加escapedstubs[i]并在中间添加parts[i]
  3. 前缀一个模板文字字符串,带有regex `...` 并返回一个新的RegExp

    const r1 = regex `^ab(c${t1}e)` /^ab(cde)/


条件转义

到目前为止,regex() 将所有模板变量 ..${..}..${..}.. 作为字符串处理。 (调用 .toString()

regex `X${new Date()}X`          // /X7\.12\.2020X/
regex `X${{}}X`                  // /X\[object Object\]X/
regex `X${[1, 'b', () => 'c']}X` // /X1,b,\(\) => 'c'X/

尽管这是预期的行为,但我们不能在所有地方使用它。

如何在“标记模板”中连接正则表达式?

Use case: You want a 'RegExp factory class' to build more complex expressions from smaller pieces. E.g. a RegExp to parse/validate Content-Type header values for javascript-like MIME types. syntax: media-type = type "/" subtype. This is what we want to find:

  • */*
  • application/*, application/javascript, application/ecmascript,
  • text/*, text/javascript, text/ecmascript
const group = (name, regExp) => regex`(?<${name}>${regExp})`;

const rTypeGroup = group(`type`, `(text|application|\*)+?`);  
// /(?<type>\(text\|application\|\*\)\+\?)/ 
const rSubGroup = group(`sub`, `((java|ecma)+?script|\*)+?`); 
// /(?<sub>\(\(java\|ecma\)\+\?script\|\*\)\+\?)/
// .. and test
rTypeGroup.test(`application`);                   // false !!!
rTypeGroup.test(`\(text\|application\|\*\)\+\?`); // true !!!

由于 regex() 转义了所有替换,因此我们的捕获组的正文与字面匹配。对于某些类型,我们可以修改 regex() 并跳过 escape()。现在我们可以传递 RegEx 实例并跳过 escape().

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");
const regex = ({
  raw: [part, ...parts]
}, ...subs) => new RegExp( //            skip escape()
  subs.reduce( //                   ┏━━━━━━━━━┻━━━━━━━━━┓ 
    (result, sub, i) => `${result}${sub instanceof RegExp ? sub.source : escape(sub)}${parts[i]}`,
    part
  )
);

const group = (name, regExp) => regex `(?<${name}>${regExp})`;

//                                         RegEx
//                               ┏━━━━━━━━━━━┻━━━━━━━━━━━┓               
const rTypeGroup = group(`type`, /(text|application|\*)+?/); //  /(?<type>(text|application|\*)+?)/
const rSubGroup = group(`sub`, /((java|ecma)+?script|\*)+?/); // /(?<sub>((java|ecma)+?script|\*)+?)/

// Type
console.log('rTypeGroup.test(`*`); ➜', rTypeGroup.test(`*`)); // true
console.log('rTypeGroup.test(`text`); ➜', rTypeGroup.test(`text`)); // true

console.log('rTypeGroup.exec(`*`).groups.type; ➜', rTypeGroup.exec(`*`).groups.type); // '*'
console.log('rTypeGroup.exec(`text`).groups.type; ➜', rTypeGroup.exec(`text`).groups.type); // 'text'

// SubType
console.log('rSubGroup.test(`*`); ➜', rSubGroup.test(`*`)); // true
console.log('rSubGroup.test(`javascript`); ➜', rSubGroup.test(`javascript`)); // true
console.log('rSubGroup.test(`ecmascript`); ➜', rSubGroup.test(`ecmascript`)); // true

console.log('rSubGroup.exec(`*`).groups.sub; ➜', rSubGroup.exec(`*`).groups.sub); // '*'
console.log('rSubGroup.exec(`javascript`).groups.sub; ➜', rSubGroup.exec(`javascript`).groups.sub); // 'javascript'

我们现在可以决定是否应该对 var 进行转义。由于我们忽略了任何量词和标志,我们的小组还验证了 ABCtextDEF12texttext、...

现在我们可以捆绑多个正则表达式:

//                                                 '/' would be escaped! RegEx needed..            
//                                                               ┏━┻━┓
const rMediaTypeGroup = group(`mediaType`, regex `${rTypeGroup}${/\//}${rSubGroup}`);
// /(?<mediaType>(?<type>(text|application|\*)+?)\/(?<sub>((java|ecma)+?script|\*)+?))/
rMediaTypeGroup.test(`text/javascript`)); // true

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");
const regex = ({
  raw: [part, ...parts]
}, ...subs) => new RegExp(
  subs.reduce(
    (result, sub, i) => `${result}${sub instanceof RegExp ? sub.source : escape(sub)}${parts[i]}`,
    part
  )
);

const group = (name, regExp) => regex `(?<${name}>${regExp})`;
const rTypeGroup = group(`type`, /(text|application|\*)+?/);
const rSubGroup = group(`sub`, /((java|ecma)+?script|\*)+?/);

//                                                 '/' would be escaped! RegEx needed..            
//                                                               ┏━┻━┓
const rMediaTypeGroup = group(`mediaType`, regex `${rTypeGroup}${/\//}${rSubGroup}`);
// /(?<mediaType>(?<type>(text|application|\*)+?)\/(?<sub>((java|ecma)+?script|\*)+?))/

console.log('rMediaTypeGroup.test(`*/*`); ➜', rMediaTypeGroup.test(`*/*`)); // true
console.log('rMediaTypeGroup.test(`**/**`); ➜', rMediaTypeGroup.test(`**/**`)); // true
console.log('rMediaTypeGroup.test(`text/javascript`); ➜', rMediaTypeGroup.test(`text/javascript`)); // true

console.log('rMediaTypeGroup.test(`1text/javascriptX`); ➜', rMediaTypeGroup.test(`1text/javascriptX`)); // true
console.log('rMediaTypeGroup.test(`*/java`); ➜', rMediaTypeGroup.test(`*/java`)); // true

console.log('rMediaTypeGroup.test(`text/X`); ➜', rMediaTypeGroup.test(`text/X`)); // false
console.log('rMediaTypeGroup.test(`/*`); ➜', rMediaTypeGroup.test(`/*`)); // false


旗帜

初始化前必须知道所有标志。您可以阅读它们(/xx/gm.flags/xx/gm.multiline/xx/i.ignoreCase、..)但是没有设置器。

A tagged template function (e.g. regex()) returning new Regex() needs to know all flags.

本节演示如何处理标志的 3 个备选方案。

  • 选项 A:将标志传递为 ${templateVariable} ➜ 不推荐!
  • 选项 B:扩展 class RegExp
  • 选项 C:对标志使用 Proxy()

选项 A:将标志传递为 ${templateVariable}。 ➜ 不推荐!

线程标志与其他替换变量一样。我们需要检查最后一个变量是否是 flag-like (g, mi,..) 并将其从 subs...

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");

const regex = ({
  raw: [part, ...parts],
  // super verbose...
  splitFlags = ({ // destruct subs[] Array (arrays are objects!)
    length: l,    // destruct subs[].length to l
    iEnd = l - 1, // last array index to iEnd
    [iEnd]: sub,  // subs[subs.length - i] to sub
    ...subs       // subs[0...n-1] to subs
  }) => [         // returns RegEx() constr. params: [flags, concat regex string]                                                    
    //             ┏━━━━━━━━ all chars of sub flag-like? ━━━━━━━━━┓   ┏━ flags  ┏━ re-add last sub and set flags: undefined
    [...sub].every(f => ['s', 'g', 'i', 'm', 'y', 'u'].includes(f)) ? sub : !(subs[iEnd] = sub) || undefined,
    Object.values(subs).reduce( // concat regex string
      (result, sub, i) => `${result}${escape(sub)}${parts[i]}`,
      part
    )
  ]
}, ...subs) => new RegExp(...splitFlags(subs).reverse());

const r1 = regex `^foo(${`bar`})${'i'}`;
console.log('r1:', r1, 'flags:', r1.flags); // /^foo(bar)/i    ['i' flag]

const r2 = regex `^foo(${`bar`})${'mgi'}`;
console.log('r2:', r2, 'flags:', r2.flags); // /^foo(bar)/gim  ['gim' flag]

//            invalid flag 'x' ━━━━┓
const r3 = regex `^foo(${`bar`})${'x'}`;
console.log('r3:', r3, 'flags:', r3.flags); // /^foo(bar)x/    [no flags]

//              invalid flag 'z' ━━━━┓
const r4 = regex `^foo(${`bar`})${'gyzu'}`;
console.log('r4:', r4, 'flags:', r4.flags); // /^foo(bar)gyzu/ [no flags]

代码看起来超级冗长,混淆 标志逻辑 替换逻辑 从外部看并不明显。如果最后一个变量被错误地确定为 flag-like.

,它也会破坏最终的正则表达式

We look for phone types, like i-phone, a-phone,...

const rPhoneA = regex `${`a`}-phone`; // 
console.log('rPhoneA:', rPhoneA, 'flags:', rPhoneA.flags); // /a-phone/     [no flags]
const rPhoneI = regex `${`i`}-phone`; 
console.log('rPhoneI:', rPhoneI, 'flags:', rPhoneI.flags); // /(?:)/i       ['i' flag]

console.log('rPhoneA.test(`a-phone`); ➜', rPhoneA.test(`a-phone`)); // true
console.log('rPhoneA.test(`i-phone`); ➜', rPhoneA.test(`i-phone`)); // false

console.log('rPhoneI.test(`a-phone`); ➜', rPhoneI.test(`a-phone`)); // true
console.log('rPhoneI.test(`i-phone`); ➜', rPhoneI.test(`i-phone`)); // true

...所有手机都是i-phones!因为 i 是一个 类似标志的 替换并且从现在为空 []subs... 数组中删除。 reduce() returns 一个空字符串 ''new RegExp('', 'i') 添加一个空的非捕获组:(?:).

选项 B:扩展 class RegExp

我们可以从 RegExp 扩展并添加 getter 方法来设置标志。使它们 自返回 以便我们可以链接它们。我们甚至可以添加一个 _“清除标志”方法。

这很好用,但也有效果,即每个标志 added/removed,都会导致 TRegExp 的新克隆。如果我们构建“静态”(可缓存)表达式,它可能适合您。

  • A:在循环内的构造函数中添加getters。或者..
  • B: 添加classgetters

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");

class TRegExp extends RegExp {

  constructor(...args) {
    super(...args);

    // Clear all flags
    Object.defineProperty(this, '_', {
      get() {
        return this.flags.length ? new TRegExp(this.source) : this;
      },
      enumerable: false
    });

    // A: define getters for all flags  
    ['g', 'i', 'm', 'u', 'y'].reduce((my, flag) => Object.defineProperty(my, flag, {
      get() {    // clone this on flags change ━━┓
        return my.flags.includes(flag) ? my : new TRegExp(my.source, `${my.flags}${flag}`);
      }, //                 return this ━━┛
      enumerable: false
    }), this);
  }

  // B: Define getters for each flag individually
  // get g() {
  //     return this.flags.includes('g') ? this : new TRegExp(this.source, `${this.flags}g`);
  // }
}

const regex = ({raw: [part, ...parts]}, ...subs) => new TRegExp(
  subs.reduce( //                                       ┣━━ TRegExp()
    (result, sub, i) => `${result}${subs instanceof TRegExp ? sub.source : escape(sub)}${parts[i]}`,
    part
  )
);

console.log('TRegExp +flags:', regex `foo(bar)`.g);       // /foo(bar)/g
console.log('TRegExp +flags:', regex `foo(bar)`.i.m);     // /foo(bar)/im
console.log('TRegExp +flags:', regex `foo(bar)`.g.i._.y); // /foo(bar)/y 
//                                              ┗━━━━━┻━━━━ ( + 'g', + 'i', - 'gi', + 'y')

const group = regex `(?<foo>(:?bar)\d{2})`.g.i;
const t = `a bar12 bar0 bar13 bar-99 xyz BaR14 bar15 abc`;
console.log([...t.matchAll(group)].map(m => m.groups.foo));
// ["bar12", "bar13", "BaR14", "bar15"]

选项 C:使用 Proxy() 作为标志

您可以代理一个“标记模板函数”,例如regex() 并拦截其上的任何 get()。代理可以解决许多问题,但也会导致极大的混乱。您可以重新编写整个函数并完全改变初始行为。

const escape = s => `${s}`.replace(/[-\/\^$*+?.()|[\]{}]/g, "\$&");

const _regex = (flags, {raw: [part, ...parts]}, ...subs) => new RegExp(
  subs.reduce(
    (result, sub, i) => `${result}${sub instanceof RegExp ? sub.source : escape(sub)}${parts[i]}`,
    part
  ),
  flags
); // ┗━━━ flags

const regex = new Proxy(_regex.bind(undefined, ''), {
  get: (target, property) => _regex.bind(undefined, property)
});


console.log('Proxy +flags:', regex.gi `foo(bar)`); // /foo(bar)/gi

const r = /(:?bar)\d{2}/; // matches: 'bar' + digit + digit ➜ 'bar12', 'abar123',..
const t = `(:?bar)\d{2}`; // template literal with regExp special chars

console.log('Proxy +flags:', regex.gi `(foo${r})`); // /(foo(:?bar)\d{2})/gi
console.log('Proxy +flags:', regex.gi `(foo${t})`); // /(foo\(:\?bar\)d\{2\})/gi
//                         flags ━┻━┛


更多阅读内容