從 RegExp 構(gòu)造器看 JS 字符串轉(zhuǎn)義設(shè)計

多年前我第一次入職騰訊的時候,DC 從杭州給我寄來了一本他剛翻譯出爐的《高性能 JavaScript》。那段時間為了幫忙校對,我仔細(xì)閱讀了書中的每一個段落,結(jié)果積累了不少 JavaScript 基礎(chǔ)知識?,F(xiàn)在還依稀記得書中提到的幾個知識點(diǎn): IE7 瀏覽器在大字符串處理時的極致性能優(yōu)化;位運(yùn)算符用于 config 配置的各種 trick;以及今天想聊的 RegExp 構(gòu)造器的第一個參數(shù)設(shè)計問題。

上周接到一個需求,根據(jù)頁面 url 來決定是否出現(xiàn)一個彈窗提示。為了方便管理這個特性,我將 url 列表配置在了后臺,前端通過接口取得列表再進(jìn)行校驗(yàn)。

其中有一條規(guī)則是「所有機(jī)構(gòu)首頁需要彈窗」,因?yàn)闄C(jī)構(gòu)會有自己的獨(dú)立二級域名,所以這里必須要用到 location.host 對應(yīng)的正則表達(dá)式 \w+\.ke\.qq\.com

new RegExp(/\w+\.ke\.qq\.com/).test('ktmaster.ke.qq.com') // 返回 true

// 由于正則表達(dá)式字符串是 cgi 接口中返回的,所以第一個參數(shù)只能用 string 類型
// 而 RegExp 構(gòu)造器使用 string 參數(shù)時,其中的 \w、\ 等特殊含義字符是需要使用反斜杠再做一層轉(zhuǎn)義,這樣同時導(dǎo)致正則語義變得很不清晰
new RegExp('\w+\.ke\.qq\.com').test('ktmaster.ke.qq.com')  // 返回 false
new RegExp('\\w+\\.ke\\.qq\\.com').test('ktmaster.ke.qq.com')  // 返回 true

然而,需求真正落地實(shí)現(xiàn)后發(fā)現(xiàn):RegExp 構(gòu)造器 string 參數(shù)需要轉(zhuǎn)義的知識點(diǎn),其實(shí)基本用不到。

1、通過接口返回的字符串在變量賦值時無需轉(zhuǎn)義

前端 AJAX 請求取到的接口數(shù)據(jù)一定是 string 類型的,這種未通過字符串字面量形式賦值給變量時是無需轉(zhuǎn)義的。以 fetchAPI 為例:

// 1. 其中 data 接口返回的內(nèi)容是 \w+\.ke\.qq\.com
fetch('/data')
  .then(res => res.text())
  .then(resText => {
    console.log(new RegExp(resText)) // 正確實(shí)例化了 /\w+\.ke\.qq\.com/
  })

// 2. 字面量形式定義的字符串不轉(zhuǎn)義,會與期望不符
const regText = '\w+\.ke\.qq\.com' // 字符串定義時 \ 會與后面一個字符合并解析掉
console.log(regText === 'w+.ke.qq.com') // 返回 true
console.log(new RegExp(regText)) // 返回的是 /w+.ke.qq.com/

現(xiàn)在大部分的接口數(shù)據(jù)會使用 JSON string,接口返回后通過 JSON.parse 成 JavaScript Object ,再通過 key 來取值。而對于 JSON 數(shù)據(jù)來說,后端 JSON.stringify 時,\ 字符是一定會經(jīng)過一層轉(zhuǎn)義的(這樣才符合 JSON 規(guī)范)。以 PHP 為例:

<?php
$regText = '\w+\.ke\.qq\.com'; // 注意 PHP 中單引號內(nèi)的字符串不會經(jīng)過解析
echo json_encode(array('pattern' => $regText));
// 返回的是 {"pattern":"\\w+\\.ke\\.qq\\.com"}

所以接口場景下,同樣不存在 RegExp 構(gòu)造器的 string 參數(shù)轉(zhuǎn)義問題。

2、表單輸入項(xiàng)的字符串賦值給變量時也無需轉(zhuǎn)義

假設(shè)頁面中存在輸入框 <input id="test"> ,在輸入框中輸入字符 \w+\.ke\.qq\.com,則通過 JS 獲取到的值可以直接傳入 RegExp 構(gòu)造器,同樣無需考慮轉(zhuǎn)義問題。

const regText = document.getElementById('test').value
new RegExp(regText) // 返回 /\w+\.ke\.qq\.com/

因?yàn)楸韱雾?xiàng)中的字符串也是直接賦值,而非通過引號字面量的字符串定義方式賦值。

3、JS 代碼中的轉(zhuǎn)義處理

另外一種可能用到 RegExp string 參數(shù)的場景是:基于 JS 邏輯,動態(tài)創(chuàng)建正則表達(dá)式。例如正則表達(dá)式 /\w{3}/ 中的數(shù)字 3,是通過某個變量來傳遞的。那么在寫正則時需要寫成:

let n = 3
new RegExp('\\w{' + n + '}') // 這里的 \w 為特殊字符,需要經(jīng)過 \ 轉(zhuǎn)義

Python 語言中是通過 raw string 修飾符來解決字符串轉(zhuǎn)義問題,在字符串前加上 r 標(biāo)記,表示這個字符串的內(nèi)容不經(jīng)過解析。即 print r'\n' == '\\n' 返回 True。

為了解決模板字符串的解析和轉(zhuǎn)義問題,ES6 模板字面量中引入了反引號(`)和 tag function(知名「CSS in JS」 庫 styled-components 中大量使用了這種語法)。這里的場景就可以寫成十分類似 Python 的風(fēng)格,當(dāng)需要轉(zhuǎn)義的內(nèi)容比較多時,能保持較好的正則表達(dá)式語義:

const r = String.raw
let n = 3
new RegExp(r`\w{${n}}`)

不過這種使用場景十分罕見,我至今還沒有遇到過。

回過頭來看,JS 正則表達(dá)式構(gòu)造器的參數(shù)設(shè)計問題,其實(shí)不是 RegExp 引起的,而是 JavaScript String 的設(shè)計缺陷:單引號和雙引號非但沒有參考 PHP/Shell 之類的設(shè)計,反而給前端社區(qū)留下「應(yīng)該使用單引號還是雙引號」的代碼風(fēng)格爭論。反觀 Golang, 在這塊的約束就做得非常好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容