多年前我第一次入职腾讯的时候,DC 从杭州给我寄来了一本他刚翻译出炉的《高性能 JavaScript》。那段时间为了帮忙校对,我仔细阅读了书中的每一个段落,结果积累了不少 JavaScript 基础知识。现在还依稀记得书中提到的几个知识点: IE7 浏览器在大字符串处理时的极致性能优化;位运算符用于 config 配置的各种 trick;以及今天想聊的 RegExp 构造器的第一个参数设计问题。
上周接到一个需求,根据页面 url 来决定是否出现一个弹窗提示。为了方便管理这个特性,我将 url 列表配置在了后台,前端通过接口取得列表再进行校验。
其中有一条规则是「所有机构首页需要弹窗」,因为机构会有自己的独立二级域名,所以这里必须要用到 location.host 对应的正则表达式 \w+\.ke\.qq\.com
new RegExp(/\w+\.ke\.qq\.com/).test('ktmaster.ke.qq.com') // 返回 true
// 由于正则表达式字符串是 cgi 接口中返回的,所以第一个参数只能用 string 类型
// 而 RegExp 构造器使用 string 参数时,其中的 \w、\ 等特殊含义字符是需要使用反斜杠再做一层转义,这样同时导致正则语义变得很不清晰
new RegExp('\w+\.ke\.qq\.com').test('ktmaster.ke.qq.com') // 返回 false
new RegExp('\\w+\\.ke\\.qq\\.com').test('ktmaster.ke.qq.com') // 返回 true
然而,需求真正落地实现后发现:RegExp 构造器 string 参数需要转义的知识点,其实基本用不到。
1、通过接口返回的字符串在变量赋值时无需转义
前端 AJAX 请求取到的接口数据一定是 string 类型的,这种未通过字符串字面量形式赋值给变量时是无需转义的。以 fetchAPI 为例:
// 1. 其中 data 接口返回的内容是 \w+\.ke\.qq\.com
fetch('/data')
.then(res => res.text())
.then(resText => {
console.log(new RegExp(resText)) // 正确实例化了 /\w+\.ke\.qq\.com/
})
// 2. 字面量形式定义的字符串不转义,会与期望不符
const regText = '\w+\.ke\.qq\.com' // 字符串定义时 \ 会与后面一个字符合并解析掉
console.log(regText === 'w+.ke.qq.com') // 返回 true
console.log(new RegExp(regText)) // 返回的是 /w+.ke.qq.com/
现在大部分的接口数据会使用 JSON string,接口返回后通过 JSON.parse 成 JavaScript Object ,再通过 key 来取值。而对于 JSON 数据来说,后端 JSON.stringify 时,\ 字符是一定会经过一层转义的(这样才符合 JSON 规范)。以 PHP 为例:
<?php
$regText = '\w+\.ke\.qq\.com'; // 注意 PHP 中单引号内的字符串不会经过解析
echo json_encode(array('pattern' => $regText));
// 返回的是 {"pattern":"\\w+\\.ke\\.qq\\.com"}
所以接口场景下,同样不存在 RegExp 构造器的 string 参数转义问题。
2、表单输入项的字符串赋值给变量时也无需转义
假设页面中存在输入框 <input id="test">
,在输入框中输入字符 \w+\.ke\.qq\.com
,则通过 JS 获取到的值可以直接传入 RegExp 构造器,同样无需考虑转义问题。
const regText = document.getElementById('test').value
new RegExp(regText) // 返回 /\w+\.ke\.qq\.com/
因为表单项中的字符串也是直接赋值,而非通过引号字面量的字符串定义方式赋值。
3、JS 代码中的转义处理
另外一种可能用到 RegExp string 参数的场景是:基于 JS 逻辑,动态创建正则表达式。例如正则表达式 /\w{3}/
中的数字 3,是通过某个变量来传递的。那么在写正则时需要写成:
let n = 3
new RegExp('\\w{' + n + '}') // 这里的 \w 为特殊字符,需要经过 \ 转义
Python 语言中是通过 raw string 修饰符来解决字符串转义问题,在字符串前加上 r 标记,表示这个字符串的内容不经过解析。即 print r'\n' == '\\n'
返回 True。
为了解决模板字符串的解析和转义问题,ES6 模板字面量中引入了反引号(`)和 tag function(知名「CSS in JS」 库 styled-components 中大量使用了这种语法)。这里的场景就可以写成十分类似 Python 的风格,当需要转义的内容比较多时,能保持较好的正则表达式语义:
const r = String.raw
let n = 3
new RegExp(r`\w{${n}}`)
不过这种使用场景十分罕见,我至今还没有遇到过。
回过头来看,JS 正则表达式构造器的参数设计问题,其实不是 RegExp 引起的,而是 JavaScript String 的设计缺陷:单引号和双引号非但没有参考 PHP/Shell 之类的设计,反而给前端社区留下「应该使用单引号还是双引号」的代码风格争论。反观 Golang, 在这块的约束就做得非常好。