在 JavaScript 中使用正则表达式

957 阅读8分钟

在 JavaScript 中使用正则表达式

几个宝藏链接

regex101.com/

regexr.com/

jex.im/regulex/#!f…

快速入门

正则表达式是匹配模式,要么匹配字符,要么匹配位置。

匹配数据

  • 正则表达式之所以强大,是因为其能实现模糊匹配。

    • 横向模糊:量词 *,+,{m,n}

      • 贪婪匹配和惰性匹配
const string = "123 1234 12345 123456";



// 贪婪匹配,越多越好

console.log(string.match(/\d{2,5}/g));

// => ["123", "1234", "12345", "12345"] 



// 惰性匹配,2 个就够了

console.log(string.match(/\d{2,5}?/g));

// => ["12", "12", "34", "12", "34", "12", "34", "56"]
  • 纵向模糊: 字符集 [abc], 多选分支(p1|p2|p3)

匹配位置

位置(锚)是指相邻字符之间的位置,我们可以将位置理解成空字符 ""

在 JS 中,共有 8 个锚: ^, $, \b, \B, (?=y), (?!y), (?<=y), (?<!y)

single charquantifiersposition
\d 匹配数字 digital [0-9]; \D [^0-9]。表示除数字外的任意字符* 0个或者更多 {0,}^一行的开头
\w 匹配 word [0-9a-zA-Z_]+ 1个或更多,至少1个 {1,}$一行的结尾
\W 匹配非 word [^0-9a-zA-Z_]? 0个或1个 {0,1}\b 单词边界 (word bounds)
\s 匹配 white space (包括空格、tab等) [ \t\v\n\r\f]{min,max} 出现次数在一个范围内\B 非单词边界
\S 匹配非 white space (包括空格、tab等){n} 匹配出现n次的(?=p) 匹配 p 前面的位置,即此位置后面匹配 p
. 通配符,表示几乎任意字符 [^\n\r\u2028\u2029]{m,} 表示至少出现 m 次。(?!p) 匹配非 p 前面的位置,即此位置后面不匹配 p
(?<=p) 匹配 p 后面的位置,即此位置前面匹配 p
(?<!p) 匹配 p 后面的位置,即此位置前面不匹配 p

括号的作用

括号提供了分组,便于我们引用分组的内容。

  1. 分组和分支结构

  • 分组:/a+/匹配连续出现的 "a",而要匹配连续出现的 "ab" 时,需要使用 /(ab)+/
  • 分支结构:多选分支结构(p1|p2) 中,括号的作用是提供了分支表达式的所有可能。
  1. 分组捕获

进行数据提取,和更强大的替换操作。

  1. 提取数据

比如提取出年、月、日,可以这么做:

const regex = /(\d{4})-(\d{2})-(\d{2})/;

const string = "2017-06-12";

console.log( string.match(regex) );

 // => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]

match 会返回一个数组。

第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。

  1. 反向引用

只能引用之前出现的分组,即反向引用。

以日期为例。 比如要写一个正则支持匹配如下三种格式:

2016-06-12

2016/06/12

2016.06.12

假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:\1

const regex = /\d{4}(-|/|.)\d{2}\1\d{2}/;

const string1 = "2017-06-12";

const string2 = "2017/06/12";

const string3 = "2017.06.12";

const string4 = "2016-06/12";

console.log(regex.test(string1)); // true

console.log( regex.test(string2) ); // true

console.log( regex.test(string3) ); // true

console.log( regex.test(string4) ); // false

注意里面的 \1,表示的引用之前的那个分组 (-|/|.)。不管它匹配到什么(比如 -),\1 都匹配那个同 样的具体某个字符。

以此类推, \2\3 分别指代第二个和第三个分组。

  1. 非捕获括号

之前文中出现的括号,都会捕获它们匹配到的数据,以便后续引用,因此也称它们是捕获型分组和捕获型分支。

如果只想要括号最原始的功能,但不引用它,可以使用非捕获括号 (?:p) (?:p1|p2|p3)

构建正则

是否有必要?

var string = '2021-06-13';

var regex = /^(\d{4})-(\d{2})-(\d{2})/;

console.log( string.match(regex) ); 

// => ["2021-06-13", "2021", "06", "13", index: 0, input: "2021-06-13", groups: undefined]



console.log( string.split('-') );

// => ["2021", "06", "13"]
var string = "?id=xx&act=search";

console.log( string.search(/?/) );

// => 0

 

console.log( string.indexOf("?") );

// => 0 

可以直接用字符串的方法满足需求。

//  要求密码长度 6-12 位,由数字、大小写字母组成,但都必须包括。

var regexp = /(?!^[0-9]{6,12}$)(?!^[A-Za-z]{6,12}$)^[0-9A-Za-z]{6,12}$/

function checkPassword1(string) {

    return regexp.test(string);

}



var regex1 = /^[0-9A-Za-z]{6,12}$/;

var regex2 = /^[0-9]{6,12}$/;

var regex3 = /^[A-Za-z]{6,12}$/;

function checkPassword (string) {

    if (!regex1.test(string)) return false;

    if (regex2.test(string)) return false;

    if (regex3.test(string)) return false;

    return true;

} 

可以拆分成几个小步骤来做。

准确性,可维护性,性能

构建一个匹配邮箱的正则

// 满足RFC 822(电子邮件的标准格式)语法的正则(http://www.ex-parrot.com/~pdw/Mail-RFC822-Address.html)

/(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*>(?:(?:\r\n)?[ \t])*)|(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*:(?:(?:\r\n)?[ \t])*(?:(?:(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*>(?:(?:\r\n)?[ \t])*)(?:,\s*(?:(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*|(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)*<(?:(?:\r\n)?[ \t])*(?:@(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*(?:,@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*)*:(?:(?:\r\n)?[ \t])*)?(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|"(?:[^"\r\]|\.|(?:(?:\r\n)?[ \t]))*"(?:(?:\r\n)?[ \t])*))*@(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*)(?:.(?:(?:\r\n)?[ \t])*(?:[^()<>@,;:\".[] \000-\031]+(?:(?:(?:\r\n)?[ \t])+|\Z|(?=[["()<>@,;:\".[]]))|[([^[]\r\]|\.)*](?:(?:\r\n)?[ \t])*))*>(?:(?:\r\n)?[ \t])*))*)?;\s*)/
function checkEmail(emailAddress) {

  var sAtom = '\w+';

  

  var sQuotedString = '"[^"]*"';

  var sWord = '(' + sAtom + '|' + sQuotedString + ')';

  var sLocalPart = sWord + '(.' + sWord + ')*';

  

  var sSubDomain = sAtom;

  var sDomain = sSubDomain + '(.' + sSubDomain + ')*';



  var sAddrSpec = sLocalPart + '@' + sDomain;

  var sValidEmail = '^' + sAddrSpec + '$';



  var reValidEmail = new RegExp(sValidEmail);  

  return reValidEmail.test(emailAddress);

}



console.log(checkEmail('chuxulu@bytedance.com'))

// => true



console.log(checkEmail('chuxuly@123@bytedance.com'))

// => false



console.log(checkEmail('chuxulu."@123"@bytedance.com'))

// => true

匹配原理

绝大部分语言自带的正则引擎都是 NFA(无穷自动机) 的,因为它们都支持了 锚点捕获组反向引用 等的高级特性。NFA 引擎的匹配方式就是我们熟悉的 DFS(深度优先搜索)。

我们可以从 DFS 的角度思考 NFA 的正则匹配过程。

NFA 引擎的匹配方式

  1. 设定起始位置;
  2. 尝试匹配;
  3. 匹配失败的话,从下一位开始继续第 2 步;
  4. 最终结果:匹配成功或失败;

什么时候会产生回溯?

  • 贪婪量词 🌰 /ab{1,3}c/ 匹配 'abbc' (如果匹配 abbbc 就没有回溯)

  • 惰性量词 🌰 /(\d{1,3}?)(\d{1,3})/ '12345'

  • 分支结构 🌰 /^(?:can|candy)$/ candy

正则引擎自身的优化

  1. 如果正则的开头是 ^,引擎会只从字符串开始进行尝试,而不去尝试其它的位置(减少搜索的初始状态);
  2. 如果正则的结尾是 $ 并且没有 +* 这种可以无限重复的量词,正则引擎会尝试从末尾的倒数若干字符进行尝试,例如 rex(-byte)?$ 最多匹配 8 个字符,因此引擎会从倒数第八个字符开始尝试(减少搜索的初始状态);
  3. 如果当前剩余的正则可以成功匹配的最小长度大于字符串的剩余长度,引擎会直接报告本次匹配失败(利用矛盾来提前剪枝);
  4. 把连续的确定字符当作一个元素,例如 \srex\d 只有三个元素:\srex\d(减少递归层数);

我们可以做的基础优化

  1. 使用更精确的元素和量词缩小查找范围

我们可以用字符集代替.,特定的量词代替 *。这样能减少很多不必要的遍历。

站在 DFS 的角度来看,这属于针对某一节点的剪枝。

  1. 优化多选分支

例如这个用来匹配域名的正则 .(?:com|org|net|cn|me|info)\b,因为更多的域名是 .com 的,引擎在更多的情况下第一次尝试就可以匹配到结果。

  1. 缩短匹配字符串

如果不幸正则的执行复杂度比较高,如果匹配字符串的长度较短(<10),那么回溯的深度相对可控,不容易引发大的性能问题。

事故报告学习

“Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.”

Cloudflare

上线一个 XSS 检测的小改动,正则发生灾难性回溯,占满 CPU 资源。

blog.cloudflare.com/details-of-…

// 肇事正则

(?:(?:"|'|]|}|\|\d|(?:nan|infinity|true|false|null|undefined|symbol|math)|`|-|+)+[)]*;?((?:\s|-|~|!|{}||||+)*.*(?:.*=.*)))



// 性能问题部分

.*.*=



// 分析

// 在前两个 .* 灾难性回溯。

// 在regex101.com(PCRE2)上调试,字符串`a{40}`会匹配 13,243 步后失败。

// 如果能改成`[^=]*=.*`,同样的字符串只需要匹配 82 步就会失败。

/".*"/ 匹配字符串 "abc"de

图中省略了尝试匹配双引号失败的过程。可以看出 .* 是非常影响效率的。

为了减少一些不必要的回溯,可以把正则修改为 /"[^"]* ``"/

Lazada

shop name 校验规则上线后,正则发生灾难性回溯,占满CPU资源。

cloud.tencent.com/developer/a…

// 肇事正则

^([A-Za-z0-9._()&'- ]|[aAàÀảẢãÃáÁạẠăĂằẰẳẲẵẴắẮặẶâÂầẦẩẨẫẪấẤậẬbBcCdDđĐeEèÈẻẺẽẼéÉẹẸêÊềỀểỂễỄếẾệỆfFgGhHiIìÌỉỈĩĨíÍịỊjJkKlLmMnNoOòÒỏỎõÕóÓọỌôÔồỒổỔỗỖốỐộỘơƠờỜởỞỡỠớỚợỢpPqQrRsStTuUùÙủỦũŨúÚụỤưƯừỪửỬữỮứỨựỰvVwWxXyYỳỲỷỶỹỸýÝỵỴzZ])+$



// 性能问题部分

[xxx]+ // 大小为 186 的字符集🤔

^([a]|[a])+$



// 分析

// 事故报告里分析性能问题的原因是字符集太大,回溯的计算量太大。但仅仅一个大的字符集,对回溯计算量的影响是线性的,不应该能把CPU打满。

// 真实的性能问题应该是:分支的两项都能匹配字符A-Z,这导致了回溯计算量是指数级。

// 在 regex101.com (PCRE2)上调试,字符串`a{10}=`会匹配 8188 步后失败。

// 如果合并字符集,就没什么性能压力了。

FreeCodeCamp

freeCodeCamp 的服务器很不稳定,下线问题代码后恢复。

www.freecodecamp.org/news/freeco…

// 肇事代码

return User.findOne$({

    where: { email: new RegExp(email.replace('.', '\.'), 'i') }

    // new RegExp("chuxulu@bytedance.com".replace('.', '\.'), "i")

    // /chuxulu@bytedance.com/i

})



// 分析

// 字符串在大规模数据中的查找效率要高于正则四五倍左右(和具体实现有关)

// 大规模数据中查找字符串尽量用字符串匹配,不建议直接用语言自带的正则。

抖音

【事故复盘】服务端下发command_patterns带有非法正则表达式导致抖音安卓Crash事故,因为是内部资料这里把链接隐藏一下。

原因是使用了非法正则表达式 \{xD83D}\{xDE00} 匹配表情包,修改 TCC 配置上线后,抖音安卓10.1.0以上用户开始冷启时 crash。

解决办法:改为 \x{DE00}

stackoverflow.com/questions/4…

总结

  1. 正则很危险,使用不当会造成很大的风险。需要提高开发对正则的性能问题的意识。
  2. 在平常写正则的时候,少写模糊匹配,越精确越好。模糊匹配、贪婪匹配、惰性匹配都会带来回溯问题,选一个影响尽可能小的方式。

补充:JS 中使用正则的注意要点

相关API注意要点

String#search, String#split, String#match, String#matchAll, String#replace,

RegExp#test, RegExp#exec

验证RegExp#test, RegExp#exec, String#search, String#match, String#replace
切分String#split
提取String#match, RegExp#exec, RegExp#test, String#search, String#replace
替换String#replace
  1. 修饰符gmatch返回格式的影响

// 没有`g`返回的是标准的正则匹配详情,有`g`返回的是所有匹配的文本(不包含详情)。

var string = '2021-06-14';

var regex1 = /\b(\d+)\b/;

var regex2 = /\b(\d+)\b/g;

console.log( string.match(regex1) );

console.log( string.match(regex2) );



// => ["2021", "2021", index: 0, input: "2021-06-14", groups: undefined]

// => ["2021", "06", "14"]
  1. 修饰符gexectest的影响

 // 正则实例上有local state,每次匹配完成后都会修改`lastIndex` 

 var regex = /a/g;

 

 console.log( regex.test('a'), regex.lastIndex );

 console.log( regex.test('aba'), regex.lastIndex ); 

 console.log( regex.test('aba'), regex.lastIndex ); 

 console.log( regex.test('aba'), regex.lastIndex );

 

 // => true 1

 // => true 3

 // => false 0 

 // => true 1
  1. split

// 第二个参数表示数组最大长度

console.log('1,2,3,4,5'.split(/,/, 2));



// => ["1", "2"]



// 正则中包含括号时,捕获到的内容会被添加到结果中

console.log('1,2,3,4,5'.split(/(,)/));



// => ["1", ",", "2", ",", "3", ",", "4", ",", "5"]



// => 可以用锚点切分字符串

console.log('123456'.split(/(?<=^.{4})/));



// => ["1234", "56"]
  1. replace的高级用法

// replace的回调中可以拿到匹配详情,可以假借替换之名,遍历匹配到的内容

var string = '2021-06-14 2021-06-15 2021-06-16';

var regex = /(\d{4})-(\d{2})-(\d{2})/g;

string.replace(regex, function (match, $1, $2, $3, index, input) {

    console.log([match, $1, $2, index, input]);

});



// => ["2021-06-14", "2021", "06", 0, "2021-06-14 2021-06-15 2021-06-16"]

// => ["2021-06-15", "2021", "06", 11, "2021-06-14 2021-06-15 2021-06-16"]

// => ["2021-06-16", "2021", "06", 22, "2021-06-14 2021-06-15 2021-06-16"]
  1. 构造函数的静态属性

//  构造函数的静态属性记录了最近一次正则操作的结果

var regex = /(?<=-)(\d{2}-(\d{2}))/g;

var string = '2021-06-14';

regex.test(string);



console.log( RegExp.input );

console.log( RegExp["$_"]);

// => "2021-06-14;"



console.log( RegExp.lastMatch );

console.log( RegExp["$&"] );

// => "06-14"



console.log( RegExp.lastParen );

console.log( RegExp["$+"] );

// => "14"



console.log( RegExp.leftContext );

console.log( RegExp["$`"] );

// => "2021-"



console.log( RegExp.rightContext );

console.log( RegExp["$'"] );

// => ";"

总结

最后我们简单总结一下。

  • 只要对正则语法有个基本的概念,遇到问题再查文档就可以了。
  • 在编写正则的时候,需要有性能相关的概念。我们可以用 DFS 思路去思考它整个匹配的过程,需要注意正则匹配失败的情况下,它递归回溯的复杂度是不是可控的。
  • 最后补充了在 JS 里面使用正则 API 需要注意的地方,全局模式可能会导致一些边界情况,还有强大的 replace 方法。