今天起床的时候检查了一下 Umi 的 issues,发现了辟舒大佬的 Feature Request,需求是写个函数,判断字符串的内容是不是合法的 JS 标识符。
辟舒大佬给了两个方案:
第二个方案好像很简单,安全不是问题,Node 有自带的沙箱模块 vm
,应该是够用了,于是我的初版方案是:
function isValid(str) {
const script = new vm.Script(`let ${str};`);
try {
script.runInNewContext()
return true;
} catch {
return false;
}
}
然后发现 vm.Script
初始化的时候就会做词法分析,这下更好了,不用执行代码就能判断,那绝对是安全啊:
function isValid(str) {
try {
new vm.Script(`let ${str};`);
return true;
} catch {
return false;
}
}
然而我多写了几个测试,发现 await
这个关键字没法检测出来,最后在 MDN 找到了答案,await
仅在模块或 async 函数下是保留字,好嘛,又是 JS 早期屎山留下的坑,那 vm 方案应该是不行的。
从上面这个方案给我们带来的教训来看,我们并不一定需要执行代码,只需要找到一个能帮我们做词法分析的东西就好了,于是我的眼光放在构建工具上。考察了一下项目内有的 babel 全家桶,最后没敢用,研究起来太费劲,而且性能你懂,所以放弃。
那就只能写正则了,正如那句老话所说的,当你有一个问题准备用正则表达式解决时,那么你就有两个问题了。
Google 试了诸多关键字,javascript valid var name regex identifier 等等,包括 npmjs 上很多库我都翻过源码了,都没找到满意的。只有各种千奇百怪的一行正则,写单测试了试,只能说总能找到方法绕过。
好在翻了好久找到了这个,这个方案是通过一百多行代码,拼出来一个能满足大部分需求的正则,这个正则的总共 2418 个字符,给我整沉默了,虽然单测能过,但是实在太逆天,只能放弃。
正在我山穷水尽,准备把这个 PR 拱手让人的时候,发现那个逆天正则的评论区下面有一个指向 tc39 一个神秘 repo 的链接。
TL;DR,总之就是 JS 内置了标识符的开头(IdentifierStart
)和其他部分(IdentifierPart
)可用的 Unicode 范围,分别可以在正则中使用 {ID_Start}
和 {ID_Continue}
直接访问。
这下问题解决了,这个提案下面连实例都写好了,不过这个正则没法匹配标识符,这个还是需要我们自己判断,我的方案是维护一个关键字数组,然后检查字符串是否在数组内,所以我的最后一个方案就写好了:
function isValidIdentifyName(name: string) {
const regexIdentifierName =
/^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u;
if (keywords.includes(name) || !regexIdentifierName.test(name)) {
return false;
}
return true;
}
于是我的 PR 就水过去了,经过一番挣扎最后也是被 approve 了,辟舒大佬的一句 LGTM 极大程度地让我这个待业青年感到了满足,让我这平凡的一天多了一丝光彩。
不过咲奈大佬在 PR 里提到可以利用 esbuild transform,可惜我根本不会,我的工程化知识只够我做点 bundle,悲伤,要抽时间好好学 esbuild 了。