parser 视角:字符就是数字
Babel 的词法分析器里几乎看不到 str[i] === '{' 这种写法——热路径上全是 code === charCodes.leftCurlyBrace:先 charCodeAt 拿到数字,再和数字常量比较。npm 包 charcodes 就是这张常量表(backSpace = 8、leftCurlyBrace = 123……),配套的 babel 插件还会把常量名内联成数字字面量,产物里连对象访问都不剩。这页拆它的两个动机:数字比较快,而且范围判断(code >= digit0 && code <= digit9)只有数字能写。
1 · 逐字符拆开:每个字符的 code 与 charcodes 名字
改下面的代码片段,看每个字符被 charCodeAt 读成什么数、在 charcodes 里叫什么。配色:蓝色是 charcodes 收录的常量、橙色是空白、红色是超出 BMP 的代理对(surrogate)。
charCodeAt 读的是 UTF-16 码元,不是字符。 试试预设里的 😀:它占两个码元(55357 / 56832,即代理对),charCodeAt 各读一半,要 codePointAt 才能拼回 128512。lexer 能用 charCodeAt 简化处理,是因为语法字符全在 ASCII 区;一旦碰到标识符里的 Unicode,就得换 codePointAt——这一现象在 regex 系列的字符编码里有完整一页。
2 · 为什么不直接比字符串?现场跑一遍
对一段 100 万字符的源码扫一遍,数出所有 {:三种写法各跑 5 轮取最优。数字比较省掉了取单字符串(可能产生临时字符串对象)与字符串相等比较:
codePointAt 为什么反而最慢?多出来的那一眼是什么。 charCodeAt 的契约是「返回第 i 个 UTF-16 码元」,实现上就是一次数组取值;codePointAt 的契约是「返回从 i 开始的完整 code point」。而 BMP 外的字符(如 😀)由 high surrogate (0xD800–0xDBFF)
加 low surrogate (0xDC00–0xDFFF) 两个码元拼成,所以 codePointAt 每读一个位置都得先判断「当前码元是不是 high surrogate」:是,就再读下一个码元、按公式合成出 > 0xFFFF 的码点;不是,才当普通码元原样返回。{(123) 是 ASCII,永远不可能是代理对,但
codePointAt 没有这层先验,这个 surrogate 分支判断对每个字符都要走一遍——一百万字符的热循环里,这一眼就是它落后于 charCodeAt 的来源。反过来也说明 lexer 敢全程用 charCodeAt,正是因为 JS 的语法字符全在 ASCII 区,扫描时根本不会踩到代理对。
下面是 charcodes 在 lexer 里的真实用法(@babel/parser 风格):识别一个数字字面量时,全程只比较 charCodeAt 返回的整数,从不构造子字符串或调用 String.prototype 方法:
import * as charCodes from 'charcodes';
function readNumber(input, pos) {
// 范围判断只有数字能写 —— 字符串比较做不到 >= <=
while (isDigit(input.charCodeAt(pos))) pos++;
if (input.charCodeAt(pos) === charCodes.dot) pos++; // '.'
while (isDigit(input.charCodeAt(pos))) pos++;
return pos;
}
function isDigit(code) {
return code >= charCodes.digit0 && code <= charCodes.digit9;
}
// babel-plugin 内联后, 产物里只剩数字:
// if (input.charCodeAt(pos) === 46) pos++;
3 · charcodes 全表(104 个常量)
下表按 code 升序列出全部 104 个导出常量,可按常量名或 code 筛选。它的取舍完全暴露用途是 JS lexer:收了 JS 规范的行终止符 lineSeparator(8232)、paragraphSeparator(8233) 和 Unicode 空白的代表 oghamSpaceMark(5760),却没收键盘上更有名的
escape(27)、delete(127);命名也随性:dash(Unicode 正式名是 hyphen-minus)、dot(正式名 full stop)、equalsTo。
4 · 相关链接
- charcodes · npm——本页的主角:Babel 团队维护的 ASCII 常量表,配 babel 插件做编译期内联。
- regex · 字符的编码:code point、UTF-16、UTF-8 · 同站——
charCodeAtvscodePointAt的完整拆解:😀 为什么length是 2。