JavaScript >> Javascript 文檔 >  >> Tags >> regex

JavaScript RegExp API 出了什麼問題,以及如何修復它

在過去的幾年裡,我偶爾會在 ES-Discuss 郵件列表上評論 JavaScript 的 RegExp API、語法和行為。最近,JavaScript 發明者 Brendan Eich 建議,為了進行更多討論,我寫了一份正則表達式更改列表,以供將來的 ECMAScript 標准考慮(或者用他幽默的話說,將我的“95 [正則表達式]論文釘在ES3 大教堂門”)。我想我會試一試,但我要把我的回答分成幾個部分。在這篇文章中,我將討論當前 RegExp API 和行為的問題。我將撇開我希望添加的新功能,而只是提出改進現有功能的方法。我將在後續帖子中討論可能的新功能。

對於像 JavaScript 這樣廣泛使用的語言,任何現實的變更提議都必須強烈考慮向後兼容性。出於這個原因,以下一些建議可能不是 特別現實,但我認為 a ) 如果不考慮向後兼容性可能會發生什麼變化是值得考慮的,並且 b ) 從長遠來看,所有這些變化都會提高 JavaScript 中正則表達式工作方式的易用性和可預測性。

移除 RegExp.prototype.lastIndex 並將其替換為起始位置的參數

實際建議:棄用 RegExp.prototype.lastIndex 並向 RegExp.prototype.exec/test 方法添加“pos”參數

JavaScript的02 屬性同時服務於太多目的:

它允許用戶手動指定從哪裡開始正則表達式搜索
你可以聲稱這不是 15 的預期目的,但它仍然是一個重要用途,因為沒有其他功能允許這樣做。 27 雖然不太擅長這個任務。您需要使用 34 編譯您的正則表達式 標誌讓 43 以這種方式使用;即使這樣,它也只指定 51 的起始位置 /68 方法。不能用於設置74的起始位置 /88 /90 /103 方法。
表示最後一場比賽結束的位置
即使您可以通過添加匹配索引和長度來得出匹配結束位置,但這種使用 110 作為對 123 的方便且常用的補充 130 返回的匹配數組的屬性 .像往常一樣,使用 141 像這樣僅適用於使用 156 編譯的正則表達式 .
用於跟踪下一次搜索應該開始的位置
這會起作用,例如,當使用正則表達式迭代字符串中的所有匹配項時。然而,164 實際上設置為最後一次匹配的結束位置,而不是下一次搜索應該開始的位置(與其他編程語言中的等價物不同)在零長度匹配後導致問題,這很容易使用像 173<這樣的正則表達式/代碼> 或 182 .因此,您不得不手動增加 195 在這種情況下。我之前已經更詳細地發布過這個問題(參見:An IE lastIndex Bug with Zero-Length Regex Matches ),和 Jan Goyvaerts 一樣(注意零長度匹配 )。

不幸的是,205 的多功能性導致它不適用於任何特定用途。我認為 219 反正是放錯地方了;如果您需要存儲搜索的結束(或下一個開始)位置,它應該是目標字符串的屬性,而不是正則表達式。以下是這會更好的三個原因:

  • 它可以讓您對多個字符串使用相同的正則表達式,而不會丟失每個字符串中的下一個搜索位置。
  • 這將允許使用具有相同字符串的多個正則表達式,並讓每個正則表達式從最後一個停止的位置開始。
  • 如果您使用相同的正則表達式搜索兩個字符串,您可能不會因為在第一個字符串中找到匹配項而期望在第二個字符串中的搜索從任意位置開始。

事實上,Perl 使用這種用字符串存儲下一個搜索位置的方法效果很好,並在其周圍添加了各種特性。

這就是我對 224 的情況 放錯地方了,但我更進一步,我不認為 238 應該包含在 JavaScript 中。 Perl 的策略適用於 Perl(尤其是當被視為一個完整的包時),但其他一些語言(包括 Python)允許您在調用正則表達式方法時提供搜索開始位置作為參數,我認為這是一種更自然的方法並且更易於開發人員理解和使用。因此,我會修復 249 通過完全擺脫它。正則表達式方法和使用正則表達式的字符串方法將使用用戶無法觀察到的內部搜索位置跟踪器,以及 259261 方法將獲得第二個參數(稱為 271 ,對於位置),指定從哪裡開始搜索。也提供 284 可能會很方便 方法 298 , 309 , 311 , 和 322 他們自己的331 參數,但這並不重要,並且它提供的功能目前無法通過 347 無論如何。

以下是 350 的一些常見用法示例 如果進行了這些更改,則可以重寫:

從位置 5 開始搜索,使用 364 (現狀):

var regexGlobal = /\w+/g,
    result;

regexGlobal.lastIndex = 5;
result = regexGlobal.test(str);
// must reset lastIndex or future tests will continue from the
// match-end position (defensive coding)
regexGlobal.lastIndex = 0;

var regexNonglobal = /\w+/;

regexNonglobal.lastIndex = 5;
// no go - lastIndex will be ignored. instead, you have to do this
result = regexNonglobal.test(str.slice(5));

從位置 5 開始搜索,使用 378

var regex = /\w+/, // flag /g doesn't matter
    result = regex.test(str, 5);

匹配迭代,使用 381

var regex = /\w*/g,
    matches = [],
    match;

// the /g flag is required for this regex. if your code was provided a non-
// global regex, you'd need to recompile it with /g, and if it already had /g,
// you'd need to reset its lastIndex to 0 before entering the loop

while (match = regex.exec(str)) {
    matches.push(match);
    // avoid an infinite loop on zero-length matches
    if (regex.lastIndex == match.index) {
        regex.lastIndex++;
    }
}

匹配迭代,使用 390

var regex = /\w*/, // flag /g doesn't matter
    pos = 0,
    matches = [],
    match;

while (match = regex.exec(str, pos)) {
    matches.push(match);
    pos = match.index + (match[0].length || 1);
}

當然,您可以輕鬆添加自己的糖來進一步簡化匹配迭代,或者 JavaScript 可以添加一個專門用於此目的的方法,類似於 Ruby 的 408 (儘管 JavaScript 已經通過使用 414 的替換函數來實現這一點 )。

重申一下,我正在描述如果向後兼容性無關緊要我會做什麼。我認為添加 426 不是一個好主意 436 的參數 和 446 方法,除非 457 由於功能重疊,該屬性已被棄用或刪除。如果 464 參數存在,人們會期望 473484 未指定時。有 490 有時會搞砸這種期望會令人困惑,並可能導致潛在的錯誤。因此,如果 502 已棄用,取而代之的是 512 ,這應該是一種接近刪除 525 的手段 完全一致。

移除String.prototype.match的非全局操作模式

實際提議:棄用 String.prototype.match 並添加新的 matchAll 方法

536 目前的工作方式非常不同,具體取決於 546 (全局)標誌已在提供的正則表達式上設置:

  • 對於帶有 555 的正則表達式 :如果沒有找到匹配項,560 被退回;否則返回一個簡單匹配的數組。
  • 對於沒有 573 的正則表達式 :581 方法作為 598 的別名運行 .如果沒有找到匹配項,605 被退回;否則你會得到一個數組,其中包含鍵零中的(單個)匹配項,任何反向引用都存儲在數組的後續鍵中。該數組還分配了特殊的 615629 屬性。

634 方法的非全局模式令人困惑且不必要。沒有必要的原因很明顯:如果你想要 645 的功能 ,只需使用它(不需要別名)。這很令人困惑,因為如上所述,658 方法的兩種模式返回非常不同的結果。不同之處不僅在於您獲得的是一場比賽還是所有比賽——您會獲得完全不同的結果。並且由於在任何一種情況下結果都是一個數組,因此您必須知道正則表達式 660 的狀態 屬性來知道你正在處理哪種類型的數組。

我會改變 674 通過使其始終返回一個包含目標字符串中所有匹配項的數組。我也會讓它返回一個空數組,而不是 687 ,當沒有找到匹配項時(來自 Dean Edwards 的 base2 庫的想法)。如果你只想要第一個匹配,或者你需要反向引用和額外的匹配細節,那就是 694 是為了。

不幸的是,如果您想將此更改視為一個現實的提議,則需要對 702 進行某種基於語言版本或模式的切換 方法的行為(我認為不太可能發生)。因此,我建議您棄用 717 方法完全支持一種新方法(可能是 721 ) 進行上述更改。

擺脫 /g 和 RegExp.prototype.global

實際建議:棄用 /g 和 RegExp.prototype.global,並在 String.prototype.replace 中添加布爾 replaceAll 參數

如果最後兩個提案得到實施,因此 731746 已成為過去(或 753 不再有時用作 766 的別名 ),773 的唯一方法 仍然會產生任何影響的是 784 .此外,雖然 799 遵循 Perl 等的現有技術,將不是正則表達式屬性的東西存儲為正則表達式標誌並沒有真正意義。真的,804 更多的是關於您希望方法如何應用它們自己的功能的聲明,並且希望在有和沒有 816 的情況下使用相同的模式並不少見 (目前您必須構建兩個不同的正則表達式才能這樣做)。如果由我決定,我會擺脫 825 flag及其對應的839 屬性,而是簡單地給出 848 方法一個附加參數,指示您是要僅替換第一個匹配項(默認處理)還是所有匹配項。這可以使用 852 來完成 布爾值,或者為了更好的可讀性,一個 867 接受值 871 的字符串 和 880 .這個新參數還有一個額外的好處,就是允許使用非正則表達式搜索替換所有功能。

請注意,SpiderMonkey 已經有一個專有的第三個 899 該提案將與之衝突的論點(“標誌”)。我懷疑這種衝突會引起很多胃灼熱,但無論如何,一個新的 901 參數將提供與 SpiderMonkey 的 915 相同的功能 參數對於(即允許使用非正則表達式搜索進行全局替換)最有用。

改變對非參與組的反向引用行為

實際提案:對非參與組的反向引用無法匹配

自從 David “liorean” Andersson 和我之前在 ES-Discuss 和其他地方為此爭論過之後,我將保持簡短。 David 在他的博客上詳細介紹了這一點(請參閱:ECMAScript 3 正則表達式:沒有意義的規範 ),並且我之前已經在這裡提到過(ECMAScript 3 正則表達式在設計上存在缺陷 )。有幾次,Brendan Eich 也表示他希望看到這種情況發生變化。對這種行為的簡短解釋是,在 JavaScript 中,對(尚未)參與匹配的捕獲組的反向引用總是成功(即它們匹配空字符串),而在所有其他正則表達式風格中則相反:它們匹配失敗,因此導致正則表達式引擎回溯或失敗。 JavaScript 的行為意味著 927 返回 932 .當推動正則表達式的邊界時,這種(負面)影響會達到相當大的程度。

我認為每個人都同意改變傳統的反向引用行為將是一種改進——它提供了更直觀的處理、與其他正則表達式風格的兼容性以及創造性使用的巨大潛力(例如,請參閱我在模仿條件上的帖子> )。考慮到向後兼容性,更大的問題是它是否安全。我認為會是這樣,因為我想或多或少沒有人故意使用不直觀的 JavaScript 行為。 JavaScript 行為相當於自動添加 949 在對非參與組的反向引用之後的量詞,如果人們實際上希望對非零長度子模式的反向引用是可選的,那麼他們已經明確地這樣做了。另請注意,Safari 3.0 及更早版本在這一點上並未遵循規範,而是使用了更直觀的行為,儘管在最近的版本中發生了變化(值得注意的是,這種變化是由於我的博客上的一篇文章而不是真實的報告——世界錯誤)。

最後,可能值得注意的是 .NET 的 ECMAScript 正則表達式模式(通過 951 flag) 確實將 .NET 切換到 ECMAScript 的非常規反向引用行為。

使\d \D \w \W \b \B 支持Unicode(如\s \S . ^ $,已經支持)

實際建議:添加一個 /u 標誌(和相應的 RegExp.prototype.unicode 屬性),以更改 \d、\w、\b 和相關標記的含義

Unicode 感知的數字和單詞字符匹配不是現有的 JavaScript 功能(缺少構建數百或數千個字符長的字符類怪物),並且由於 JavaScript 缺乏後向性,您無法重現 Unicode 感知的單詞邊界。因此,您可以說此提案超出了本文所述範圍,但我將其包含在此處是因為我認為這更像是一個修復而不是一個新功能。

根據當前的 JavaScript 標準,966 , 975 , 980 , 991 , 和 1007 使用基於 Unicode 的 空格 解釋 和 換行 ,而 1011 , 1022 , 1033 , 1041 , 1057 , 和 1069 使用 digit 的純 ASCII 解釋 , 單詞字符 , 和 單詞邊界 (例如,1079 不幸返回 1089 )。請參閱我關於 JavaScript、Regex 和 Unicode 的帖子 了解更多詳情。為這些令牌添加 Unicode 支持會導致數千個網站出現意外行為,但可以通過新的 1090 安全地實現它 標誌(靈感來自 Python 的 11041116 flag) 和相應的 1127 財產。因為它實際上是相當普遍的 not 如果希望這些令牌在特定的正則表達式模式中啟用 Unicode,則激活 Unicode 支持的新標誌將提供兩全其美。

在子模式重複期間更改反向引用重置的行為

實際建議:不要在比賽期間重置反向引用值

與上一個反向引用問題一樣,David Andersson 在他的帖子 ECMAScript 3 正則表達式:一個沒有意義的規範中也提到了這一點 .這裡的問題涉及通過捕獲嵌套在量化的外部組(例如,1133 )。根據傳統行為,量化分組中的捕獲組記住的值是該組上次參加比賽時匹配的值。所以,1145 的值 在 1155 之後 用於匹配1168 將是 1179 .然而,根據 ES3/ES5,對嵌套分組的反向引用的值在外部分組重複後被重置/擦除。因此,1180 仍會匹配 1198 , 但匹配完成後 1203 將引用一個非參與的捕獲組,在 JavaScript 中將匹配正則表達式本身內的一個空字符串,並作為 1216 返回 例如,1224 返回的數組 .

我的改變理由是當前的 JavaScript 行為打破了其他正則表達式風格的規範,不適合各種類型的創意模式(請參閱我關於 Capturing Multiple, Optional HTML Attribute Values 的帖子中的一個示例 ),並且在我看來遠不如更常見的替代正則表達式行為那麼直觀。

我相信這種行為可以安全地改變,原因有兩個。首先,對於除核心正則表達式嚮導之外的所有正則表達式嚮導,這通常是一個邊緣案例問題,我會驚訝地發現依賴於 JavaScript 版本的這種行為的正則表達式。其次,更重要的是,Internet Explorer 並沒有實現這個規則,而是遵循更傳統的行為。

添加 /s 標誌,已經

實際建議:添加一個 /s 標誌(和相應的 RegExp.prototype.dotall 屬性),更改 dot 以匹配包括換行符在內的所有字符

我將把它作為更改/修復而不是新功能偷偷加入,因為使用 1231 並不難 當您想要 1248 的行為時代替點 .我認為 1258 到目前為止,flag 已被排除在外以保護新手並限制失控回溯的損害,但最終發生的情況是人們編寫了非常低效的模式,例如 1262 而是。

JavaScript 中的正則表達式搜索很少是基於行的,因此希望 dot 包含換行符比匹配除換行符之外的任何內容更為常見(儘管這兩種模式都很有用)。保留點的默認含義(無換行符)是很有意義的,因為它由其他正則表達式共享並且需要向後兼容,但添加了對 1278 的支持 標誌過期了。指示是否設置此標誌的布爾值應在正則表達式上顯示為名為 1289 的屬性 (來自 Perl、.NET 等的不幸名稱)或更具描述性的 1291 (用於Java、Python、PCRE等)。

個人喜好

以下是一些符合我偏好的更改,儘管我認為大多數人不會認為它們是重大問題:

  • 允許正則表達式在字符類中使用未轉義的正斜杠(例如,1301 )。這已經包含在廢棄的 ES4 變更提案中。
  • 允許未轉義的 1317 作為字符類中的第一個字符(例如,13201335 )。這可能在所有其他正則表達式風格中都是允許的,但會創建一個空類,後跟一個文字 1345 在 JavaScript 中。我想想像沒有人故意使用空類,因為它們不能始終如一地跨瀏覽器工作,並且有廣泛使用/常識性的替代方案(1350 而不是 1362 , 和 1374 而不是 1389 )。不幸的是,在 Acid3(測試 89)中測試了對這個 JavaScript 怪癖的遵守情況,這很可能足以終止對這種向後不兼容但合理的更改的請求。
  • 更改1393 1409 替換字符串中使用的標記 .這很有意義。 (用於比較的其他替換文本風格中的等效項:Perl:1414; Java:1427; .NET:1432 , 1447; PHP:1459 , 1468;紅寶石:1472 , 1489; Python:1493 .)
  • 去掉1500的特殊含義 .在字符類中,元序列 1512 匹配一個退格字符(相當於 1523 )。這是一種毫無價值的便利,因為沒有人關心匹配退格字符,而且鑑於 1532 在字符類之外使用時匹配單詞邊界。儘管這會打破正則表達式的傳統(我通常提倡遵循),但我認為 1545 在字符類中應該沒有特殊含義,只需匹配文字 1558 .

在 ES3 中修復:刪除八進製字符引用

ECMAScript 3 從正則表達式語法中刪除了八進製字符引用,儘管 1561 被保留為一個方便的例外,它允許輕鬆匹配 NUL 字符。但是,瀏覽器通常保留完整的八進制支持以實現向後兼容性。八進制在正則表達式中非常令人困惑,因為它們的語法與反向引用重疊,並且在字符類之外允許額外的前導零。考慮以下正則表達式:

  • 1572 :1582 是八進制數。
  • 1594 :1609 是反向引用。
  • 1618 :1623 是八進制數。
  • 1634 :1640 是反向引用; 1651 是八進制數。
  • 1665 :1677的所有出現 和 1684 是八進制。但是,根據 ES3+ 規範,每個 1691 之後的數字 應該被視為(除非是非標準擴展)作為文字字符,完全改變這個正則表達式匹配的內容。 (Edit-2012:實際上,仔細閱讀規範表明 1700 之後的任何 0-9 應該導致 1710 .)
  • 1729 :1730 字符類之外是八進制;但在內部,八進制在第三個零處結束(即,字符類匹配字符索引零 1748 )。因此,此正則表達式等效於 1759;儘管如前所述,堅持 ES3 會改變含義。
  • 1760 :在字符類之外,八進制在第四個零處結束,後跟一個文字 1773 .在內部,八進制在第三個零處結束,後面是文字 1789 .再一次,ES3 排除八進制並包含 1798 可能會改變意思。
  • 1802 :鑑於在 JavaScript 中,對尚未(尚未)參與的捕獲組的反向引用匹配空字符串,此正則表達式是否匹配 1810 (即,1820 被視為反向引用,因為相應的捕獲組出現在正則表達式中)還是匹配 1838 (即 1843 被視為八進制,因為它出現在之前 其對應的組)?毫不奇怪,瀏覽器不同意。
  • 1859 :現在事情變得非常棘手。此正則表達式是否匹配 1865 , 1874 , 1881 , 1899 , 1903 , 或 1911 ?所有這些選項似乎都是合理的,並且瀏覽器在正確的選擇上存在分歧。

還有其他需要擔心的問題,例如八進制轉義符是否達到 1929 (1932 , 8 位)或 1944 (1950 , 9 位);但無論如何,正則表達式中的八進制是一個令人困惑的集群問題。儘管 ECMAScript 已經通過刪除對八進制的支持來清理這個爛攤子,但瀏覽器並沒有效仿。我希望他們會這樣做,因為與瀏覽器製造商不同,我不必擔心這一點遺留問題(我從不在正則表達式中使用八進制,你也不應該)。

在 ES5 中修復:不緩存正則表達式

根據 ES3 規則,如果在同一個腳本或函數中已經使用了具有相同模式/標誌組合的字面量,則正則表達式字面量不會創建新的正則表達式對象(這不適用於由 1960 構造函數)。這樣做的一個常見副作用是使用 1978 的正則表達式文字 flag 沒有他們的 1988 在大多數開發人員期望的某些情況下重置屬性。一些瀏覽器沒有遵循關於這種不直觀行為的規範,但 Firefox 做到了,因此它成為 Mozilla 重複次數第二多的 JavaScript 錯誤報告。幸運的是,ES5 擺脫了這個規則,現在每次遇到正則表達式時都必須重新編譯(這種變化在 Firefox 3.7 中出現)。

———
所以你有它。我已經概述了我認為 JavaScript RegExp API 出錯的地方。您是否同意所有這些建議,或者 您是否不必擔心向後兼容性?有沒有比我建議的更好的方法來解決這裡討論的問題?對現有的 JavaScript 正則表達式功能還有其他不滿嗎?我很想听聽對此的反饋。

由於我在這篇文章中一直關注負面因素,我會注意到我發現在 JavaScript 中使用正則表達式通常是一種愉快的體驗。 JavaScript 做對了很多事情。


Tutorial JavaScript 教程
  1. 使用 AngularJS 創建一個簡單的購物車:第 2 部分

  2. 使用 Firebase 和 Nuxt 登錄 Google

  3. redux-workerized - 用於 React 和 Vue 的 WebWorker 中的 Redux

  4. Javascript Promise 初學者指南

  5. MooTools 中的 Sizzle 和 Peppy 選擇器引擎

  6. React 的人性化介紹(和 JSX,以及一些 ES 6)

  7. JavaScript 中的觀察者設計模式

  1. Javascript removeEventListener 不起作用

  2. 如何使用nodejs在二維碼中間添加logo

  3. jQuery替換頁面上的所有文本

  4. 8 個偉大的開發者擴展⚙️ 為您的瀏覽器🧑‍💻

  5. 如何僅在多個其他功能完成後才執行 Javascript 功能?

  6. #JS - 使用代理操作對象

  7. 如何以編程方式關閉 notify.js 通知?

  1. WebRTC - 使用 JavaScript 的簡單視頻聊天(第 2 部分)

  2. 如何在命令行上創建 npm 包版本的差異

  3. 使用 Vue 和 Markdown 創建一個簡單的博客

  4. React Table App:列出用於排序和分組的電子商務產品