JavaScript 的后行断言

4,329 阅读2分钟
原文链接: unadlib.github.io
  • (?<=pattern) 零宽正向后行断言(zero-width positive lookbehind assertion)
  • (?<!pattern) 零宽负向后行断言(zero-width negative lookbehind assertion)

一直以来有个问题:JavaScript的正则表达式并不支持后行断言(RegExp Lookbehind Assertions)(也有叫后顾或反向预查),毕竟在ECMA 262 5.1/6.0/7.0中完全没有提及,在2015年底至2016年初,也有提议在TC-39讨论加入ECMA-262草案中,其原因据说是早起ECMAScript3引入正则的时候,ECMAScript3已经相对稳定,参考Perl的正则,当时Perl的也正在实验。虽然原因有点含糊其辞,但不可能是有人以讹传讹说后行断言(任意模式)性能问题过于占用CPU资源而被摒弃。虽然连ECMA-262 7.0都没有被添加,但是在2016年年初,V8引擎在4.9(–harmony)以及之后版本均已先行实现,当然也包括从Chrome 49开始在开启about:flags中【实验性JavaScript】即可。

Mac:

新建一个 Automater 应用, 然后选择 Run Shell Script 使用open命令并编辑所需要的参数:
open /Applications/Google\ Chrome.app --args --js-flags="--harmony-regexp-lookbehind"

Windows:

chrome.exe --js-flags="--harmony-regexp-lookbehind"

node:

node --harmony_regexp_lookbehind

直到ES5.1,依然不支持的正则特性(JavaScript高级程序设计):

  1. 匹配字符串开始的结尾的A和Z锚(但支持以^和$来匹配字符串的开始的结尾)
  2. 后行断言(但完全支持先行断言)
  3. 并集和交集类
  4. 原子组(atomic grouping)
  5. Unicode支持(单个字符除外,如\uFFFF)
  6. 命名的捕获组(但支持编号的捕获组)
  7. s(single,单行)和x(free-spacing,无间隔)匹配模式
  8. 条件匹配
  9. 正则表达式注释

实现后行断言

function prepareLb(lb) {
    // Allow mode modifier before lookbehind
    var parts = /^((?:\(\?[\w$]+\))?)\(\?<([=!])([\s\S]*)\)$/.exec(lb);
    return {
        // $(?!\s) allows use of (?m) in lookbehind
        lb: XRegExp(parts ? parts[1] + "(?:" + parts[3] + ")$(?!\\s)" : lb),
        // Positive or negative lookbehind. Use positive if no lookbehind group
        type: parts ? parts[2] === "=" : !parts
    };
}

有人用正则匹配实现了一个初级的方法,并可以合并到xregexp,它利用?:规则简单实现了compile/exec/test,以及search/match/replace/split,且不支持非后行断言起始或多组后行断言。

String.prototype.reverse = function () {
	return this.split('').reverse().join('');
};
// Mimicking lookbehind like (?<=es)t
var output = 'testt'.reverse().replace(/t(?=se)/g, 'x').reverse();
// output: tesxt

XRegExp的创建者Steven Levithan在2007年的Blog中提到几个实现方法,其中捕获组包括?:reverse->先行断言->reverse之类的。

ps.在测试过程中偶然发现一个有趣的对比(较早来源Laruence):

console.time();/^(t+)+\1$/.exec("tttttttttttttttttttttttttttest");console.timeEnd();
Chrome/54.0.2840.98                default: 1.36e+03ms
Safari/10.0.1                      default: 48.115ms
FireFox/49.0(并不支持console.time)  default: 1451ms

同样采用NFA类型的正则引擎,JavaScriptCore与V8在这点优化上,差别那么大,有点意外;看来在,backtracking和lazy上应该有区别,有时间好看看它们之间算法(难道是!(string.length)排列组合过大?);并且如果测试的字符串再长点,Chrome 就直接卡住,并且不会内存溢出,CPU温度直接飙到85摄氏度,Chrome任务管理器的当前页面进程直接100%。