阅读视图

发现新文章,点击刷新页面。
🔲 ☆

JS正则表达式y标识符之粘性匹配

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=12076
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

一、前言概述

当年我是捧着JavaScript高级语言设计这本书学习JS正则表达式的,知识基本上都停留在那个时期。

最近偶然发现,正则表达式还支持sticky粘性标识,使用字母y表示。

看了下支持的时间,距今也有五六年的时间了,已经谈不上新特性了。

粘性正则兼容性

趁着春节前比较有空,赶快学习一番。

二、y标识符基础常识

粘性匹配的标识符是y

顺便回顾下其他标识符,全局是g,不缺分大小写是i,多行是m

以上知识都是所有前端开发人员都需要掌握的。

必不可少的lastIndex

粘性匹配在实际使用的时候,一定要指定lastIndex,因为他的含义就是指定索引位置的匹配。

例如:

const str = "table football";
const regex = /foo/y;

regex.lastIndex = 6;

console.log(regex.test(str));
// 输出结果是: true

console.log(regex.test(str));
// 输出结果是: false

上面的示意代码,第一个regex.test(str)之所以为true,是因为字符串"table football"的索引6位置是空格,正好后面的字符就是 foo

而第二个regex.test(str)返回值是false是因为粘性匹配完成后,如果匹配,则lastIndex自动定位到匹配字符的结尾,也就是tball,自然就返回false

如果粘性定位匹配失败,那么lastIndex会变成0.

下图就是运行结果示意:

粘性定位运行结果示意

三、使用粘性匹配的场景

粘性匹配y标识符适合具有规律结构的复杂字符串匹配。

例如解析 Token(标记化)、构建词法分析器、解析特定格式数据流。

下面以解析一段简单的 CSS 声明块示意:

const cssInput = "color: #fff; display: block; margin: 20px;";

// 定义 Sticky 正则
// 匹配 "属性名: 值;" 这种结构,并允许属性名前后有可选空格
const propRegex = /\s*([a-z-]+)\s*:\s*([^;]+)\s*;/y;

function parseCSS(input) {
  const declarations = [];
  
  // 只要匹配成功,propRegex.lastIndex 就会自动更新到下一次匹配的起点
  while (true) {
    const match = propRegex.exec(input);
    
    if (match) {
      const [fullMatch, property, value] = match;
      declarations.push({ property, value: value.trim() });
    } else {
      // 检查是否是因为解析到了末尾而停止,还是因为遇到了非法格式
      if (propRegex.lastIndex < input.length) {
        console.warn(`解析中断,剩余内容不符合 CSS 格式。`);
      }
      break;
    }
  }
  
  return declarations;
}

const result = parseCSS(cssInput);
console.table(result);

输出的结果如下图所示:

CSS解析示意

极致的性能优化

在处理长文本时,Sticky 模式具有显著的性能优势。

我们不妨假设一个场景,在这个场景下,我们已知目标内容应该出现在索引 n 处。

此时可以对比下:

  • 全局模式 (/pattern/g): 如果位置 n 不匹配,引擎会继续扫描 n+1, n+2 直到文本结束。
  • Sticky 模式 (/pattern/y): 如果位置 n 不匹配,立即停止并返回 null。这避免了在大规模文本中进行无谓的全量扫描。

模拟锚点匹配

这个场景……也算不上什么优势,只能说是个额外实现技巧。

在非多行模式下,lastIndex0的Sticky正则其行为类似于带了行首锚点 ^ 的正则。

所以如果我们希望强制正则从头开始匹配,且不希望在正则字符串里硬编码 ^,可以使用 y 标志。

例如:

/^\d+/

可以写成:

/\d+/y

四、其他些补充及结语

我们可以借助RegExp.prototype.sticky判断一个正则是不是粘性匹配的。

例如:

const regex = /foo/y;
console.log(regex.sticky);
// 返回结果: true

想想看,还有没有其他遗漏的。

哦,有个细节,就是exec()test()方法的一个差异,按照MDN文档的说法:

对于exec()方法,同时具有粘性(sticky)和全局(global)特性的正则表达式与同时具有粘性和非全局特性的正则表达式行为相同。由于test()是exec()的简单封装,因此它会忽略全局标志,同样执行粘性匹配。

就我个人而言,exec()方法很少使用,所以,上面的细节差异,我也懒得深究了。

好了,就说这么多吧,我们春节后再见!

春节快乐

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=12076

(本篇完)

🔲 ☆

JS正则新特性:安全过滤RegExp.escape方法

by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=11765
本文可全文转载,但需要保留原作者、出处以及文中链接,AI抓取保留原文地址,任何网站均可摘要聚合,商用请联系授权。

RegExp.escape静态函数

一、RegExp.escape静态方法

正则对象RegExp新增了一个名为escape()的静态方法,可以对字符串中不安全的,或者可以影响正则匹配的字符进行转义。

这些在正则中具有特殊含义的字符包括,如点号(.)、星号(*)、问号(?)、加号(+)、花括号({})、方括号([])、圆括号(())、竖线(|)和反斜杠()等。

目前所有现代浏览器都已经支持了该特性。

escape正则兼容性

语法

语法比较简单,如下所示:

RegExp.escape(string)

返回转义后的字符串。

转义规则

RegExp.escape()方法的转义规则还比较复杂,像我,之前在生产环境做类似转义处理的时候,只会对正则相关的字符前面加反斜杠处理,但这只是RegExp.escape()方法的规则之一。

这些规则包括:

  • 首字母如果是数字或者字母(如论大小写),都会使用\x转义法表示。例如:
    console.log(RegExp.escape('CSS世界'));
    // 输出:"\x43SS世界"
  • 正则语法字符,就是^, $, \, ., *, +, ?, (, ), [, ], {, }, and | 以及 /会使用添加反斜杠的方式转义。例如:
    console.log(RegExp.escape('好书:CSS选择器世界+CSS新世界'));
    // 输出:"好书:CSS选择器世界\+CSS新世界"
  • 其他一些标点,如,, -, =, <, >, #, &, !, %, :, ;, @, ~, ', `"会使用\x转义法表示。例如:
    console.log(RegExp.escape('日期:2025-07-15'));
    // 输出:"日期:2025\x2d07\x2d15"
  • 具有自己的字符转义序列的字符:\f(U+000C 换页)、\n(U+000A 换行),\r(U+0000D 回车)、\t(U+0009 制表符)和\v(U+000B 行制表)转义后会使用斜杠+字符的表示方法,例如换行:
    console.log(RegExp.escape(`作者
    张鑫旭`));
    // 输出:"作者:\n张鑫旭"
  • 空格字符会转义为\x20
  • 其他非ASCII换行符和空格字符被替换为一个或两个\uXXXX转义序列,表示它们的UTF-16代码单元。
    console.log(RegExp.escape('特殊 空格'));
    // 输出:"特殊\u2005空格"
  • Lone surrogates(孤立代理项)使用\uXXXX转义序列替换。

Lone surrogates补充说明:

在计算机字符串处理中,Lone surrogates(孤立代理项) 是UTF-16编码中出现的无效或未配对的代理项代码单元(surrogate code units),属于Unicode标准中的异常情况。

JS中2024年新增的isWellFormed()和toWellFormed()就是用来检测和处理这类字符的。

以上就是转义规则细节,当然,我们平时使用的时候,并不需要了解这么多。

只需要知道,以后使用 new RegExp()构造正则表达式的时候,里面的字符串都用这里的escape()方法转义一下就可以。

那为何要转义呢?下面通过案例演示下转义的必要性。

二、使用案例

比如说我有一个URL输入框,但是这个输入框只能输入白名单域名的地址:

<input id="input" type="url" name="url">
const arrDomain = ['www.zhangxinxu.com', 'www.cssworld.cn', 'www.canvasapi.cn'];

很多人会想到使用正则实现,例如:

const arrReg = arrDomain.map(domain => {
  return new RegExp(`https?://${domain}(?=/)?`, 'g')
});
// 是否匹配白名单域名
console.log(arrReg.some(reg => reg.test(input.value)));

当我们使用域名进行测试的时候,似乎逻辑都OK,但实际上是有漏洞的,域名中的字符’.’被当做任意字符解析了,因此,如果用户输入的value值是https://www-zhangxinxu.com也会匹配:

const arrDomain = ['www.zhangxinxu.com', 'www.cssworld.cn', 'www.canvasapi.cn'];

const arrReg = arrDomain.map(domain => {
  return new RegExp(`https?://${domain}(?=/)?`, 'g')
});

// 下面返回的是true
console.log(arrReg.some(reg => reg.test('https://www-zhangxinxu.com')));

所以,需要我们对特殊字符进行转义,这就需要用到本文的RegExp.escape()静态方法了。

使用示意:

const arrDomain = ['www.zhangxinxu.com', 'www.cssworld.cn', 'www.canvasapi.cn'];

const arrReg = arrDomain.map(domain => {
  return new RegExp(`https?://${RegExp.escape(domain)}(?=/)?`, 'g')
});

// 下面返回的是false
console.log(arrReg.some(reg => reg.test('https://www-zhangxinxu.com')));

就不会有安全问题了。

当然,我们只是为了实现上述需求,还有更好的做法:

arrDomain.includes(new URL(input.value).hostname);

不过,new URL()解析是有可能会报错的。

此时,我们可以使用浏览器原生的验证能力进行处理,如下:

if (input.validity.valid) {
  console.log(arrDomain.includes(new URL(input.value).hostname));
} else {
  console.log(false);
}

Node环境上面的方法就不适合啦!

三、结语

附上传统转义过滤处理示意:

function escapeRegExp(str) {
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  // $&表示匹配的子串
}

最后总结下:

RegExp.escape是一个极其有用的工具,用于安全地将字符串转换为正则表达式字面量。它通过转义所有特殊字符,防止了正则表达式的意外行为和安全问题。

在处理用户输入或动态构建正则表达式时,使用RegExp.escape是一种安全可靠的最佳实践。

对于需要更高安全性的应用场景,建议结合其他输入验证和清理技术,构建多层次的防御策略。

好了,以上就是本文的全部内容。

我家宋玉长老希望你可以转发此文!

宋玉长老

😉😊😇
🥰😍😘

本文为原创文章,会经常更新知识点以及修正一些错误,因此转载请保留原出处,方便溯源,避免陈旧错误知识的误导,同时有更好的阅读体验。
本文地址:https://www.zhangxinxu.com/wordpress/?p=11765

(本篇完)

❌