Akara's Wonderland

判断字符串是否是合法的标识符

2023-02-06 · 4 min read

今天起床的时候检查了一下 Umi 的 issues,发现了辟舒大佬的 Feature Request,需求是写个函数,判断字符串的内容是不是合法的 JS 标识符。

辟舒大佬给了两个方案:

  1. 使用正则表达式匹配
  2. 使用类似 eval 的方式,然后判断是否报错,有安全问题

第二个方案好像很简单,安全不是问题,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 了。