在 JavaScript 中模仿 Lookbehind
與前瞻不同,JavaScript 不支持正則表達式後瞻語法。這很不幸,但我並不滿足於僅僅接受這個事實。以下是我想出的三種在 JavaScript 中模仿lookbehinds 的方法。
對於那些不熟悉lookbehinds概念的人來說,它們是零寬度斷言,就像更具體的\b
, ^
, 和 $
元字符,實際上不要使用 任何東西——它們只是匹配文本中的一個位置。這可能是一個非常強大的概念。如果您需要更多詳細信息,請先閱讀此內容。
使用 replace 方法和可選的捕獲組模仿後視
第一種方法不太像真正的後視,但在一些簡單的情況下它可能“足夠好”。舉幾個例子:
// Mimic leading, positive lookbehind like replace(/(?<=es)t/g, 'x') var output = 'testt'.replace(/(es)?t/g, function($0, $1){ return $1 ? $1 + 'x' : $0; }); // output: tesxt // Mimic leading, negative lookbehind like replace(/(?<!es)t/g, 'x') var output = 'testt'.replace(/(es)?t/g, function($0, $1){ return $1 ? $0 : 'x'; }); // output: xestx // Mimic inner, positive lookbehind like replace(/\w(?<=s)t/g, 'x') var output = 'testt'.replace(/(?:(s)|\w)t/g, function($0, $1){ return $1 ? 'x' : $0; }); // output: text
不幸的是,在許多情況下,使用此構造無法模仿後視。舉個例子:
// Trying to mimic positive lookbehind, but this doesn't work var output = 'ttttt'.replace(/(t)?t/g, function($0, $1){ return $1 ? $1 + 'x' : $0; }); // output: txtxt // desired output: txxxx
問題是正則表達式依賴於實際消耗應該在零寬度後向斷言內的字符,然後如果反向引用包含或不包含值,則簡單地放回未違反的匹配(有效的無操作)。由於此處的實際匹配過程不像真正的後視那樣起作用,因此這只適用於有限數量的場景。此外,它僅適用於 replace
方法,因為其他與正則表達式相關的方法不提供動態“撤消”匹配的機制。但是,由於您可以在替換函數中運行任意代碼,因此它確實提供了有限的靈活性。
通過反轉模仿向後看
下一種方法使用前瞻來模擬後瞻,並依賴於手動反轉數據並向後編寫正則表達式。如果將其與 replace
一起使用,您還需要將替換值向後寫入 方法,如果將其與 search
一起使用,則翻轉匹配索引 方法等。如果這聽起來有點令人困惑,那就是。稍後我將展示一個示例,但首先我們需要一種方法來反轉我們的測試字符串,因為 JavaScript 本身並不提供此功能。
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
這實際上工作得很好,並且允許模仿正面和負面的後視。但是,編寫一個所有節點都反轉的更複雜的正則表達式可能會有點混亂,而且由於前瞻用於模擬後瞻,因此您不能將您想要的內容作為真實的前瞻混合在同一模式中。
請注意,反轉字符串並使用反轉節點應用正則表達式實際上可以開闢全新的方法來處理模式,並且在某些情況下可能會使您的代碼更快,即使有反轉數據的開銷 .我將不得不將效率討論留到另一天,但在繼續第三種後視模仿方法之前,這裡有一個通過反轉實現的新模式方法的示例。
在我的上一篇文章中,我使用以下代碼為所有前面沒有點、字母或下劃線的數字從右邊每三位添加逗號:
String.prototype.commafy = function () { return this.replace(/(^|[^\w.])(\d{4,})/g, function($0, $1, $2) { return $1 + $2.replace(/\d(?=(?:\d\d\d)+(?!\d))/g, '$&,'); }); }
這是另一種實現方式:
String.prototype.commafy = function() { return this. reverse(). replace(/\d\d\d(?=\d)(?!\d*[a-z._])/gi, '$&,'). reverse(); };
我將把分析留給您。
最後,我們來看看第三種lookbehind-mimicking方法:
使用 while 循環和 regexp.lastIndex 模仿後視
最後一種方法有以下優點:
- 它更易於使用(無需反轉您的數據和正則表達式節點)。
- 它允許將lookahead 和lookbehind 一起使用。
- 它可以讓您更輕鬆地自動化模仿過程。
然而,權衡是,為了避免干擾標準的正則表達式回溯,這種方法只允許您在正則表達式的開頭和/或結尾使用lookbehinds(正或負)。幸運的是,在正則表達式的開頭使用lookbehind 是很常見的。
如果您還不熟悉 exec
RegExp
可用的方法 對象,請確保在繼續之前在 Mozilla 開發人員中心閱讀它。特別是,看看使用 exec
的例子 在 while
內 循環。
下面是這種方法的一個快速實現,我們實際上會在其中玩弄正則表達式引擎的顛簸機制,讓它按照我們的意願工作:
var data = 'ttttt', regex = /t/g, replacement = 'x', match, lastLastIndex = 0, output = ''; regex.x = { gRegex: /t/g, startLb: { regex: /t$/, type: true } }; function lookbehind (data, regex, match) { return ( (regex.x.startLb ? (regex.x.startLb.regex.test(data.substring(0, match.index)) === regex.x.startLb.type) : true) && (regex.x.endLb ? (regex.x.endLb.regex.test(data.substring(0, regex.x.gRegex.lastIndex)) === regex.x.endLb.type) : true) ); } while (match = regex.x.gRegex.exec(data)) { /* If the match is preceded/not by start lookbehind, and the end of the match is preceded/not by end lookbehind */ if (lookbehind(data, regex, match)) { /* replacement can be a function */ output += data.substring(lastLastIndex, match.index) + match[0].replace(regex, replacement); if(!regex.global){ lastLastIndex = regex.gRegex.lastIndex; break; } /* If the inner pattern matched, but the leading or trailing lookbehind failed */ } else { output += match[0].charAt(0); /* Set the regex to try again one character after the failed position, rather than at the end of the last match */ regex.x.gRegex.lastIndex = match.index + 1; } lastLastIndex = regex.x.gRegex.lastIndex; } output += data.substring(lastLastIndex); // output: txxxx
這是相當多的代碼,但它非常強大。它考慮了使用前導和尾隨的lookbehind,並允許使用替換值的函數。此外,這可以相對容易地製成一個函數,該函數使用普通的lookbehind語法接受正則表達式的字符串(例如,“(?<=x)x(?<!x)
"),然後在應用之前將其拆分為需要的各個部分。
備註:
regex.x.gRegex
應該是regex
的精確副本 , 不同的是它必須使用g
標記是否regex
確實(為了exec
與while
交互的方法 根據需要循環)。regex.x.startLb.type
和regex.x.endLb.type
使用true
對於“積極”和false
為“否定”。regex.x.startLb.regex
和regex.x.endLb.regex
是您要用於後視的模式,但它們必須包含尾隨$
.在這種情況下,美元符號並不意味著 數據結束 , 而是 它們將被測試的數據段的結尾 .
如果您想知道為什麼沒有任何關於固定長度與可變長度後視的討論,那是因為這些方法都沒有任何此類限制。它們支持完整的、可變長度的lookbehind,除了.NET 和JGsoft(由RegexBuddy 等產品使用)之外,我所知道的任何正則表達式引擎都無法做到這一點。
總之,如果您利用上述所有方法,則在絕大多數情況下都可以在 JavaScript 中模仿正則表達式後視語法。如果您對這些內容有任何反饋,請務必使用評論按鈕。
2012 年 4 月更新: 請參閱我的後續博客文章,JavaScript Regex Lookbehind Redux ,我在其中發布了一組簡短的函數,可以更輕鬆地模擬領先的後視。