Babel 的核心之一就是其转译模块,包括 babel-core
、babel-parser
等。Babel 的转译过程分为"解析/转换/生成"三步。
转译环节 | 操作 | 功能 |
---|---|---|
解析 | 词法分析 | 生成标记数组(tokens) |
语法分析 | 根据 tokens 数组生成源码的树形结构,即抽象语法树 | |
语义分析 | 向抽象语法树补充源码的语义信息,如作用域、重复定义检查等 | |
转换 | 借助 Babel 插件对抽象语法树进行转换 | 生成目标抽象语法树 |
生成 | 根据抽象语法树对象,重新生成代码字符串 | 声明目标代码字符串 |
本章主要分为两部分讲解 Babel 相关的转译原理,分别是"理论知识"和"Babel的转译过程"。阅读顺序建议先了解转译原理的理论知识。
《编译原理》本身是大学的计算机相关课程之一,Babel 用到的理论也是基于其中的部分原理,可惜笔者学过的知识早就还给老师了。
为了帮助开发者了解 Babel 的基本原理,Babel 官网推荐了一个项目: the-super-tiny-compiler
,除去注释代码仅 200 行,可读性很好,建议阅读,作为了解 Babel 原理的前菜。
# the-super-tiny-compiler
# 解析目标
the-super-tiny-compiler
项目解析的目标是将 lisp 风格的函数调用转换为 C 风格。
假设有两个函数,add
和 subtract
,那么它们的写法将会是下面这样:
LISP C
2 + 2 (add 2 2) add(2, 2)
4 - 2 (subtract 4 2) subtract(4, 2)
2 + (4 - 2) (add 2 (subtract 4 2)) add(2, subtract(4, 2))
2
3
4
5
# 解析过程
# 词法单元
the-super-tiny-compiler
定义了如下词法单元:
语素 | 标记类型 |
---|---|
( | paren |
) | paren |
/[0-9]/ | number |
/[a-z]/i | name |
其中,简略起见,空格没有作为输出的 tokens
中的一员;name
也仅限于字母组成,没有覆盖 add3
这种含字母和数字的情况。
# 代码逻辑
the-super-tiny-compiler
定义了方法 tokenizer(input: string): Array<{ type: String; value: String }>
作为词法分析器。
这个方法不到 100 行,整体运行流程如下:
function tokenizer(input) {
// `current` 字符串扫描过程中指向当前位置的指针
var current = 0;
// `tokens` 数组,也是要返回的对象,数组项为各个 token 单元
var tokens = [];
// 首先创建一个 `while` 循环, `current` 变量会在循环中自增
while (current < input.length) {
// 我们在这里储存了 `input` 中 `current` 位置的字符
var char = input[current];
// token 类型: `paren`,左圆括号
// 检查一下是不是一个左圆括号
if (char === '(') {
// 如果是,那么我们 push 一个 type 为 `paren`,value 为左圆括号的对象。
tokens.push({
type: 'paren',
value: '('
});
// 发现了一个 token,自增 `current`
current++;
// 结束本次循环,进入下一次循环
continue;
}
// token 类型: `paren`,右圆括号
// 然后我们检查是不是一个右圆括号。这里做的时候和之前一样: 检查右圆括号、加入新的 token、
// 自增 `current`,然后进入下一次循环。
if (char === ')') {
tokens.push({
type: 'paren',
value: ')'
});
// 发现了一个 token,自增 `current`
current++;
// 结束本次循环,进入下一次循环
continue;
}
// 过滤空格,空格对 token 而言是无意义的
var WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
// 自增 `current`
current++;
// 结束本次循环,进入下一次循环
continue;
}
// token 类型: `number`,数字
// 下一个 token 的类型是数字。它和之前的一个字符的 token 不同,因为数字可以由多个数字字符组成,
// 但是只能把它们识别为一个 token。
//
// (add 123 456)
// ^^^ ^^^
// 这里只有两个 token
//
// 当遇到一个数字字符时,会进入判断逻辑
var NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 创建一个 `value` 字符串,记录数字字符串
var value = '';
// 进入子循环,直到遇到的字符不是数字字符为止,
// 将循环途中遇到的每个数字拼接到 `value` 字符串中,并且自增 `current`
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 然后将类型为 `number` 的 token 放入 `tokens` 数组中
tokens.push({
type: 'number',
value: value
});
// 结束本次循环,进入下一次循环
continue;
}
// token 类型: `name`,函数名
// 最后一种类型的 token 是 `name`。它由一系列的字母组成,在 lisp 语法中代表函数。
//
// (add 2 4)
// ^^^
// Name token
//
var LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
var value = '';
// 进入子循环,直到遇到的字符不是字母为止。注意,这里没有考虑含有非字母的函数名写法。
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 添加一个类型为 `name` 的 token
tokens.push({
type: 'name',
value: value
});
// 结束本次循环,进入下一次循环
continue;
}
// 最后如果没有匹配上任何类型的 token,则抛出一个错误。
throw new TypeError('I dont know what this character is: ' + char);
}
// 词法分析器的最后返回 tokens 数组。
return tokens;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
# 词法分析
the-super-tiny-compiler
项目里,用到了"词法分析"。
词法分析(lexical analysis)是将字符序列转换为标记(token)的过程。进行词法分析的程序或函数叫做词法分析器(lexical analyzer,简称 lexer),也叫扫描器(scanner)。
以英文句子 "today is comfortable."
为例,词法分析用于这个句子中是否有不合格的"单词",以及将"单词"进行分类,这个"单词"也就是标记(token)。
# 相关概念
# 标记(token)
标记(token
)是构成源码的最小单位。从输入的字符串流中生成标记的过程叫标记化(tokenization
),在这个过程中,词法分析器也会对标记进行分类。
标记(token)通常是一个对象,包括了"种别码"和"属性值"(token<种别码, 属性值>
)。
以 the-super-tiny-compiler 输出的 token 为例,"种别码"也就是 "number/paren/name" 等类型的描述,"属性值"(也就是 "123" 等值)。
词法分析器通常不会关注标记之间的关系(这由语法分析器关注),比如,词法分析可以将括号识别为标记(token
),但无法保证括号间是否匹配。
在词法分析过程中,会将代码字符串解析为一个个扁平的不可分割的词法单元,比如:
const a = 1;
这段代码被词法分析后,会分割为如下词法单元。
属性值 | 种别码 |
---|---|
const | const |
a | name |
= | eq |
1 | num |
; | semi |
这里采用的是 Babel 内部定义的种别码,不是普遍标准。
语法分析器读取输入字符流、从中识别出语素、最后生成不同类型的标记。其间一旦发现无效标记,便会报错。
https://tc39.es/ecma262/#prod-ReservedWord
# 标识符(Identifier)
标识符指的是变量、函数、属性的名字、函数的参数。
标识符的书写规则: (规则是哪里来的?)
第一个字符必须是字母、下划线(
_
)或者是美元符号($
),不能为数字name √ _say √ $hi √ 3name ×
1
2
3
4第一个字符之后,可以是字母、下划线(
_
)或者是美元符号($
)、数字name1 √ n_ame √ n$ √
1
2
3不能将关键字、保留字用作标识符
name √ function ×
1
2可以使用 Unicode 转义序列。例如,字符
a
可以使用"\u0061
"表示\u0061 = 1 √
1
# 关键字和保留字
关键字
关键字是 ECMA-262 规定的 JavaScript 语言内部使用的一组名称(或称为命令)。这些名称具有特定的用途。
关键字首先是标识名(
IdentifierName
),同时具有语义(也就是在 JavaScript 中有特殊用途),比如if/while/async/await
等。关键字不能作为标识符(
Identifier
),比如变量、标签和函数名,以下这些写法是错误的:const if = 1; × var while = 2; × function await() {} ×
1
2
3
4
5保留字
保留字是 ECMA-262 规定的 JavaScript 语言内部预备使用的一组名称(或称为命令)。这些名称目前还没有具体的用途,是为 JavaScript 升级版本预留备用的,建议用户不要使用。
保留字从字面意思来看,就是"未来的关键字",本身还不具有特殊含义,但将来是可能成为关键字的。
保留字不能作为标识符(
Identifier
)。保留字分为无条件保留字和有条件保留字。无条件保留字指的是本身严格为保留字,没有任何条件,如
if/while
;有条件保留字指的是在特定的上下文中才被视为保留字,比如await
只是在async
函数/模块中,才被视为保留字。关键字与保留字列举
部分关键字同时也是保留字,部分并不是。有些关键词只在特定的上下文语义中被识别为保留字。
名称 是关键字 是保留字 await
× 在 async
函数/模块中,才被视为保留字break
√ √ case
√ √ catch
√ √ class
√ √ const
√ √ continue
√ √ debugger
√ √ default
√ √ delete
√ √ do
√ √ else
√ √ enum
√ √ export
√ √ extends
√ √ false
√ √ finally
√ √ for
√ √ function
√ √ if
√ √ import
√ √ in
√ √ instanceof
√ √ new
√ √ null
√ √ return
√ √ super
√ √ switch
√ √ this
√ √ throw
√ √ true
√ √ try
√ √ typeof
√ √ var
√ √ void
√ √ while
√ √ with
√ √ yield
√ √ async
√ × abstract
× √ double
× √ goto
× √ native
× √ static
× √ boolean
× √ implements
× √ package
× √ byte
× √ private
× √ synchronized
× √ char
× √ extends
× √ int
× √ protected
× √ throws
× √ final
× √ interface
× √ public
× √ transient
× √ float
× √ long
× √ short
× √ volatile
× √
# 词法分析过程
总的来说,词法分析分为 2 个步骤: "扫描、标记"。
# 第 1 步: 扫描识别 token
扫描是词法分析的第一阶段。词法分析器会扫描代码字符串,将其分隔为一个个 token,同时也是边扫描边识别 token 的。
这里涉及 2 个问题:
代码字符串是怎样被分隔为一个个 token 的?
利用有限状态机。
识别 token 的依据是什么?
利用字符串匹配和正则表达式。
# 有限状态机
问题拆解
有限状态机(Finite State Mechine),又称有限状态自动机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
它有如下特征:
状态总数(state)是有限的 任一时刻,只处在一种状态之中 某种条件下,会从一种状态转变(transition)到另一种状态
在词法分析中,扫描源码时,遇到不同的字符,会进入不同的"状态",这个"状态"有标识符、操作符、数字、字符串等。整个源码的扫描过程就是不同"状态"的迁移过程。
比如,待扫描的源码为:
height >= 20
,我们自定义的状态转译过程:其中,涉及的状态有:
- 标识符: 标记为
name
。第一个字符必须是字母,后面的字符可以是字母或数字 - 比较操作符: 标记为
largerthan
,就是>=
- 数字字面量: 标记为
num
,全部由数字构成
对于 JavaScript 而言,很显然这些状态的定义不是很完整,比如"数字字面量"还可以包含小数点,我们自定义的状态定义里缺失了这一点,不过对于我们理解有限状态机已经足够了。
并且,上图并不是真正的优先状态机的描述方式,正确的描述方式是:
- 标识符: 标记为
状态转移过程
空状态
刚开始启动或从其他状态返回的默认空状态。
标识符状态
初始状态时,遇到的第一个字符是字母的话,会转移到"标识符状态"。
然后,继续"预扫描"后续的字符。
如果"预扫描"到的下一个字符是字母或数字,则存储该字符到 token 对象的
value
属性,并停留在"标识符状态";如果"预扫描"到的下一个字符不是字母或数字,则回退到上一个字符(也就是"回溯"),记录当前 token 为"标识符",并退回"空状态"。
数字字面量状态
空状态时,遇到的第一个字符是数字的话,会转译到"数字字面量状态"。
然后,继续"预扫描"后续的字符。
如果"预扫描"到的下一个字符是数字,则存储该字符到 token 对象的
value
属性,并停留在"数字字面量状态";如果"预扫描"到的下一个字符不是数字,则回退到上一个字符(也就是"回溯"),记录当前 token 为"数字字面量状态",并退回"空状态"。
回溯
在描述状态转译过程时,提到了"回溯"。
回溯法是一种暴力搜索法,采用“试错”的思想,它尝试分步的去解决一个问题。在分步解决问题的过程中,当它通过尝试发现,现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算,再通过其它的可能的分步解答再次尝试寻找问题的答案。
总结
通过上述分析过程,可以看到,词法分析的过程,其实就是根据构造的有限状态自动机,通过回溯等方式,持续进行状态转移,从而解析出各个
token
。例子中只是包含了 3 个状态的有限状态自动机,要构造一个强大的词法分析器,可以根据场景扩展状态数量。
# 正则表达式
这里的"正则表达式"就是 JavaScript 中的正则表达式,在进行状态判断时,通常用正则表达式判断状态类型,比如上文中我们自定义的状态对应的正则如下:
- 标识符:
/[a-zA-Z]([a-zA-Z_]|[0-9])*/
- 数字字面量:
/\d+/
- 赋值操作符:
/=/
当然,也可以用字符串匹配来识别各类状态,词法分析器大量采用了正则表达式和字符串匹配进行状态识别。
Babel 的词法分析器组合使用了正则表达式/字符串匹配(重度使用了 charcodes
这个 npm 包)进行状态识别,后文会详细介绍。
# 第2步: 标记 token
扫描器识别出可标记的字符序列后,会交由标记生成器处理,标记生成器处理的过程叫标记化(tokenization)。
标记化处理时,会将输入字符串分割为标记、进而将标记进行分类。这样生成的标记会被用于语法分析。
比如字符串: The quick brown fox jumps over the lazy dog
。
计算机并不知道这是以空格分隔的九个英语单词,只知道这是普通的 43 个字符构成的字符串。可以通过一定的方法(这里使用空格作为分隔符)将语素(这里即英语单词)从输入字符串中分割出来。分割后的结果用 XML 可以表示如下:
<sentence>
<word>The</word>
<word>quick</word>
<word>brown</word>
<word>fox</word>
<word>jumps</word>
<word>over</word>
<word>the</word>
<word>lazy</word>
<word>dog</word>
</sentence>
2
3
4
5
6
7
8
9
10
11
尽管在某些情况下需要手工编写词法分析器,一般情况下词法分析器都用自动化工具生成。
TODO
# Babel 的词法分析
Babel 的词法分析模块定义在 Tokenizer
模块,对应的目录是 babel/packages/babel-parser/src/tokenizer/
,并且,Tokenizer
会作为后续语法分析、语义分析的一个中间模块提供服务。
# Babel 的词法单元
Babel 的 Tokenizer
模块定义了基本的词法单元,并深度依赖了第三方模块 charcodes
辅助进行词法分析。
内置词法单元
文件
babel/packages/babel-parser/src/tokenizer/types.js
列举了 Babel 支持的所有词法单元。语素 Babel代码中的助记符 简介 分类 num
num
数字 bigint
bigint
用于表示大于 2^53 - 1 的整数 decimal
decimal
更语义化地表示数字,如 1_000_000
regexp
regexp
正则 string
string
字符串 name
name
名称 eof
eof
end of file
bracketL
bracketL
[
字符标点符号 bracketHashL
bracketHashL
#[
字符标点符号 bracketBarL
bracketBarL
[\|
字符标点符号 bracketR
bracketR
]
字符标点符号 bracketBarR
bracketBarR
\|]
字符标点符号 bracketR
bracketR
}
字符标点符号 braceL
braceL
{
字符标点符号 braceBarL
braceBarL
{\|
字符标点符号 braceHashL
braceHashL
#{
字符标点符号 braceR
braceR
}
字符标点符号 braceBarR
braceBarR
\|}
字符标点符号 parenL
parenL
(
字符标点符号 parenR
parenR
)
字符标点符号 comma
comma
,
字符标点符号 semi
semi
;
字符标点符号 colon
colon
:
字符标点符号 doubleColon
doubleColon
::
字符标点符号 dot
dot
.
字符标点符号 question
question
?
字符标点符号 questionDot
questionDot
?.
字符标点符号 arrow
arrow
=>
字符标点符号 template
template
template
字符标点符号 ellipsis
ellipsis
...
字符标点符号 backQuote
backQuote
``` 字符 标点符号 dollarBraceL
dollarBraceL
${
字符标点符号 at
at
@
字符hash
hash
#
字符标点符号 interpreterDirective
interpreterDirective
#!
字符eq
eq
=
字符操作符 assign
assign
_=
字符操作符 slashAssign
slashAssign
_=
字符操作符 incDec
incDec
++
/--
字符操作符 bang
bang
!
字符操作符 tilde
tilde
~
字符操作符 pipeline
pipeline
\|>
字符操作符 nullishCoalescing
nullishCoalescing
??
字符操作符 logicalOR
logicalOR
\|\|
字符操作符 logicalAND
logicalAND
&&
字符操作符 bitwiseOR
bitwiseOR
\|
字符操作符 bitwiseXOR
bitwiseXOR
^
字符操作符 bitwiseAND
bitwiseAND
&
字符操作符 equality
equality
==
/!=
/===
/!==
字符操作符 relational
relational
<
/>
/<=
/>=
字符操作符 bitShift
bitShift
<<
/>>
/>>>
字符操作符 plusMin
plusMin
+
/-
字符操作符 modulo
modulo
%
字符操作符 star
star
*
字符操作符 slash
slash
/
字符操作符 exponent
exponent
**
字符操作符 break
_break
break
字符关键词 case
_case
case
字符关键词 catch
_catch
catch
字符关键词 continue
_continue
continue
字符关键词 debugger
_debugger
debugger
字符关键词 default
_default
default
字符关键词 do
_do
do
字符关键词 else
_else
else
字符关键词 finally
_finally
finally
字符关键词 for
_for
for
字符关键词 function
_function
function
字符关键词 if
_if
if
字符关键词 return
_return
return
字符关键词 switch
_switch
switch
字符关键词 throw
_throw
throw
字符关键词 try
_try
try
字符关键词 var
_var
var
字符关键词 const
_const
const
字符关键词 while
_while
while
字符关键词 with
_with
with
字符关键词 new
_new
new
字符关键词 this
_this
this
字符关键词 super
_super
super
字符关键词 class
_class
class
字符关键词 extends
_extends
extends
字符关键词 export
_export
export
字符关键词 import
_import
import
字符关键词 null
_null
null
字符关键词 true
_true
true
字符关键词 false
_false
false
字符关键词 in
_in
in
字符关键词 instanceof
_instanceof
instanceof
字符关键词 typeof
_typeof
typeof
字符关键词 void
_void
void
字符关键词 delete
_delete
delete
字符关键词 charcodes
Babel 利用社区的 npm 包
charcodes
帮助进行词法单元的识别,charcodes
定义的词法单元如下:https://compart.com/en/unicode/U+2029
charcodes 命名 编码 十进制数字 说明 backSpace
U+0008
8 tab
U+0009
9 \t
lineFeed
U+000A
10 \n
carriageReturn
U+000D
13 \r
shiftOut
U+000E
14 space
U+0020
32 空格 exclamationMark
U+0021
33 !
quotationMark
U+0022
34 "
numberSign
U+0023
35 #
dollarSign
U+0024
36 $
percentSign
U+0025
37 %
ampersand
U+0026
38 &
apostrophe
U+0027
39 '
leftParenthesis
U+0028
40 (
rightParenthesis
U+0029
41 )
asterisk
U+002A
42 *
plusSign
U+002B
43 +
comma
U+002C
44 ,
dash
U+002D
45 -
dot
U+002E
46 .
slash
U+002F
47 /
digit0
U+0030
48 0
digit1
U+0031
49 1
digit2
U+0032
50 2
digit3
U+0033
51 3
digit4
U+0034
52 4
digit5
U+0035
53 5
digit6
U+0036
54 6
digit7
U+0037
55 7
digit8
U+0038
56 8
digit9
U+0039
57 9
colon
U+003A
58 :
semicolon
U+003B
59 \|
lessThan
U+003C
60 <
equalsTo
U+003D
61 =
greaterThan
U+003E
62 >
questionMark
U+003F
63 ?
atSign
U+0040
64 @
uppercaseA
U+0041
65 A
uppercaseB
U+0042
66 B
uppercaseC
U+0043
67 C
uppercaseD
U+0044
68 D
uppercaseE
U+0045
69 E
uppercaseF
U+0046
70 F
uppercaseG
U+0047
71 G
uppercaseH
U+0048
72 H
uppercaseI
U+0049
73 I
uppercaseJ
U+004A
74 J
uppercaseK
U+004B
75 K
uppercaseL
U+004C
76 L
uppercaseM
U+004D
77 M
uppercaseN
U+004E
78 N
uppercaseO
U+004F
79 O
uppercaseP
U+0050
80 P
uppercaseQ
U+0051
81 Q
uppercaseR
U+0052
82 R
uppercaseS
U+0053
83 S
uppercaseT
U+0054
84 T
uppercaseU
U+0055
85 U
uppercaseV
U+0056
86 V
uppercaseW
U+0057
87 W
uppercaseX
U+0058
88 X
uppercaseY
U+0059
89 Y
uppercaseZ
U+005A
90 Z
leftSquareBracket
U+005B
91 [
backslash
U+005C
92 \
rightSquareBracket
U+005D
93 ]
caret
U+005E
94 ^
underscore
U+005F
95 _
graveAccent
U+0060
96 ` lowercaseA
U+0061
97 a
lowercaseB
U+0062
98 b
lowercaseC
U+0063
99 c
lowercaseD
U+0064
100 d
lowercaseE
U+0065
101 e
lowercaseF
U+0066
102 f
lowercaseG
U+0067
103 g
lowercaseH
U+0068
104 h
lowercaseI
U+0069
105 i
lowercaseJ
U+006A
106 j
lowercaseK
U+006B
107 k
lowercaseL
U+006C
108 l
lowercaseM
U+006D
109 m
lowercaseN
U+006E
110 n
lowercaseO
U+006F
111 o
lowercaseP
U+0070
112 p
lowercaseQ
U+0071
113 q
lowercaseR
U+0072
114 r
lowercaseS
U+0073
115 s
lowercaseT
U+0074
116 t
lowercaseU
U+0075
117 u
lowercaseV
U+0076
118 v
lowercaseW
U+0077
119 w
lowercaseX
U+0078
120 x
lowercaseY
U+0079
121 y
lowercaseZ
U+007A
122 z
leftCurlyBrace
U+007B
123 {
verticalBar
U+007C
124 \|
rightCurlyBrace
U+007D
125 }
tilde
U+007E
126 ~
nonBreakingSpace
U+00A0
160 不换行空格 oghamSpaceMark
U+1680
5760 lineSeparator
U+2028
8232 paragraphSeparator
U+2029
8233
# Babel tokenizer 源码解析
# 文件结构
Babel Tokenizer 的核心代码位于目录 github/babel/packages/babel-parser/src/tokenizer/
,其中 /tokenizer/index.js
定义了 Tokenizer
类。
文件结构:
|-- babel-parser
|-- src
|-- tokenizer
|-- context.js: 上下文对象 context,与 token 示例关联
|-- index.js: 定义了 Tokenizer 类,包括各种 token 相关的处理方法和属性
|-- state.js: 维护了当前 token 实例的状态信息
|-- types.js: 维护了所有 token 类型
2
3
4
5
6
7
# tokendizer/index.js 重点代码
总体而言,tokendizer/index.js 中定义的 Tokenizer
类继承自 PaserErrors
对象,同时定义了关于 token 的各种操作方法和属性。
tokens: Array<Token | N.Comment> = [];
token
数组。数组项可以是Token
类的实例或者是注释对象。nextToken()
访问下一个
token
。笔者在源码中添加了部分注释,便于理解。
nextToken(): void { const curContext = this.curContext(); // 如果不保留空白符和注释,则跳过它们 // 一般情况下,均认为空格是可以忽略的,不需要识别为 token。有几种类型的 token 是例外: // 1. `template`: 模板字符串中的空格是有意义的 // 2. jsx 中的 `expr`: `<tag>...</tag>` 标签内的空格也是有意义的 if (!curContext.preserveSpace) this.skipSpace(); this.state.start = this.state.pos; // 记录访问开始的位置信息 if (!this.isLookahead) this.state.startLoc = this.state.curPosition(); // 如果目标访问位置超过了源码长度,则认为进入源码文件末尾,也就是 "end-of-file" if (this.state.pos >= this.length) { this.finishToken(tt.eof); return; } // 区分是在读取 template 语法还是非 template 语法(数字、操作符等) if (curContext === ct.template) { this.readTmplToken(); } else { this.getTokenFromCode(this.codePointAtPos(this.state.pos)); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27lookahead()
读取并返回下一个
token
对象。这里用到了简单的"回溯"写法,先保存当前 token 对象,再直接进入并获取下一个 token 对象,最后恢复为当前 token 对象。lookahead(): LookaheadState { // 保留状态 const old = this.state; // 创建一个新的 token state this.state = this.createLookaheadState(old); // 访问下一个 token this.isLookahead = true; this.nextToken(); this.isLookahead = false; const curr = this.state; // 恢复状态 this.state = old; // 返回下一个 token return curr; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20codePointAtPos()
TODO
# Babel 定义的 token 类型
词法文法: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Lexical_grammar JavaScript 参考: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference
在介绍词法分析时提到过一个案例:
const a = 1;
这段代码被词法分析后,会分割为如下词法单元。
词法单元 | token 类型 |
---|---|
const | const |
a | name |
= | eq |
1 | num |
; | semi |
很显然这些类型对 Babel 来说是不够的,笔者整理了 Babel tokendizer 中的 token 类型。
# "空白"
"空白"包括: 格式控制符、空白符、行终止符:
格式控制符
用于控制对源码文本的解释,但是并不会显示出来。
空白符
用于提升源码的可读性,并不影响源码的功能,通常用于将标记(tokens)分开。
行终止符
除了"空白符"之外,"行终止符"也可以提高源码的可读性。
不同的是,"行终止符"可以影响 JavaScript 代码的执行。"行终止符"也会影响自动分号补全的执行。在正则表达式中,"行终止符"会被
\s
匹配。
下表是各种"空白"的介绍:
分类 | 编码 | 名称 | 缩写 | 说明 | 转义序列 |
---|---|---|---|---|---|
格式控制符 | U+200C | 零宽不连字 | <ZWNJ> | 放置在一些经常会被当成连字的字符之间,用于将它们分别以独立形式显示 | |
格式控制符 | U+200D | 零宽连字 | <ZWJ> | 放置在一些通常不会被标记为连字的字符之间,用于将这些字符以连字形式显示 | |
格式控制符 | U+FEFF | 字节流方向标识 | <BOM> | 在脚本开头使用,除了将脚本标记为 Unicode 格式以外,还用来标记文本的字节流方向 | |
空白符 | U+0009 | 制表符 | <HT> | 水平制表符,就是TAB | \t |
空白符 | U+000B | 垂直制表符 | <VT> | 垂直制表符,就是垂直方向的TAB | \v |
空白符 | U+000C | 分页符 | <FF> | 分页符 | \f |
空白符 | U+0020 | 空格 | <SP> | 空格 | |
空白符 | U+00A0 | 不换行空格 | <NBSP> | <SP> 的变体,非换行空格,在文字排版中可以避免因为空格在此处发生断行,在 HTML 中使用的   会生成它 | |
空白符 | Others | 其他 Unicode 空白 | <USP> | ||
行终止符 | U+000A | 换行符 | <LF> | 在 UNIX 系统中起新行 | \n |
行终止符 | U+000D | 回车符 | <CR> | 在 Commodore 和早期的 Mac 系统中起新行 | \r |
行终止符 | U+2028 | 行分隔符 | <LS> | Unicode 中的行分隔符 | |
行终止符 | U+2029 | 段分隔符 | <PS> | Unicode 中的段落分隔符 |
Babel 中,有一个单独的文件提供了"空白"相关的工具方法: babel/packages/babel-parser/src/util/whitespace.js。
其核心代码如下:
const lineBreak = /\r\n?|[\n\u2028\u2029]/;
// https://tc39.github.io/ecma262/#sec-line-terminators
export function isNewLine(code: number): boolean {
switch (code) {
case charCodes.lineFeed: // "\n" <LF>
case charCodes.carriageReturn: // "\r" <CR>
case charCodes.lineSeparator: // 行分隔符 <LS>
case charCodes.paragraphSeparator: // 段分隔符 <PS>
return true;
default:
return false;
}
}
// https://tc39.github.io/ecma262/#sec-white-space
export function isWhitespace(code: number): boolean {
switch (code) {
case 0x0009: // CHARACTER TABULATION
case 0x000b: // LINE TABULATION
case 0x000c: // FORM FEED
case charCodes.space: // 空格 <SP>
case charCodes.nonBreakingSpace: // 不换行空格 <NBSP>
case charCodes.oghamSpaceMark: // ?
case 0x2000: // EN QUAD
case 0x2001: // EM QUAD
case 0x2002: // EN SPACE
case 0x2003: // EM SPACE
case 0x2004: // THREE-PER-EM SPACE
case 0x2005: // FOUR-PER-EM SPACE
case 0x2006: // SIX-PER-EM SPACE
case 0x2007: // FIGURE SPACE
case 0x2008: // PUNCTUATION SPACE
case 0x2009: // THIN SPACE
case 0x200a: // HAIR SPACE
case 0x202f: // NARROW NO-BREAK SPACE
case 0x205f: // MEDIUM MATHEMATICAL SPACE
case 0x3000: // IDEOGRAPHIC SPACE
case 0xfeff: // ZERO WIDTH NO-BREAK SPACE
return true;
default:
return false;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 注释
注释用来在源码中增加提示、笔记、建议、警告等信息,可以帮助阅读和理解源码。在调试时,可以用来将一段代码屏蔽掉,防止其运行。
在 JavaScript 中,有 3 种添加注释的方法。
单行注释:
//
后的文本都视为注释// 这是单行注释 console.log('hello world!');
1
2多行注释:
/* */
/* * 这是多行注释 1 * 这是多行注释 2 */ console.log('hello world!');
1
2
3
4
5Hashbang 注释:
#!
#!/usr/bin/env node console.log('hello world!');
1
2JavaScript 中的 hashbang 注释模仿 Unix 中的
shebangs
,用于指定适当的解释器运行文件,它仅在脚本或模块的绝对开头有效。在计算领域中,Shebang(也称为 Hashbang)是一个由井号和叹号构成的字符序列
#!
,其出现在文本文件的第一行的前两个字符。 在文件中存在Shebang
的情况下,类 Unix 操作系统的程序加载器会分析Shebang
后的内容,将这些内容作为解释器指令,并调用该指令,并将载有Shebang
的文件路径作为该解释器的参数。例如,以指令
#!/bin/sh
开头的文件在执行时会实际调用/bin/sh
程序。下面列出了一些典型的 shebang 解释器指令:
#!/bin/sh
— 使用sh
,即 Bourne shell或其它兼容
shell` 执行脚本#!/bin/csh
— 使用csh
,即 C shell 执行#!/usr/bin/perl -w
— 使用带警告的 Perl 执行#!/usr/bin/python -O
— 使用具有代码优化的 Python 执行#!/usr/bin/php
— 使用 PHP 的命令行解释器执行
文件 babel/packages/babel-parser/src/tokenizer/index.js 中有与注释相关的处理方法,tokenizer 对注释的处理方式分为两种:
单行和多行注释,视为"空白"
skipSpace()
方法会识别注释并添加到commentStack
数组中。识别时,以
/
为关注点,检测下一个字符是*
或/
。如果下一个字符是
*
,进入skipBlockComment()
的执行流程;如果下一个字符是
/
,进入skipLineComment()
的执行流程。Hashbang 注释的处理方法
readToken_interpreter(): boolean { // 如果字符起始位置不是第一个字符,就直接返回,不做处理 // 如果长度不够 2,也不做处理 if (this.state.pos !== 0 || this.length < 2) return false; let ch = this.input.charCodeAt(this.state.pos + 1); if (ch !== charCodes.exclamationMark) return false; // ! const start = this.state.pos; this.state.pos += 1; while (!isNewLine(ch) && ++this.state.pos < this.length) { ch = this.input.charCodeAt(this.state.pos); } // 取出 #! xxx 中的 xxx 作为 value 值 const value = this.input.slice(start + 2, this.state.pos); this.finishToken(tt.interpreterDirective, value); return true; }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22该方法会识别并取出
#! xxx
中的xxx
作为该 token 的值。
# 标识符: name
标识符是代码中用来标识变量、函数、或属性的字符序列。
在 JavaScript 中,标识符只能包含字母或数字或下划线("_"
)或美元符号("$"
),且不能以数字开头。
文件 babel/packages/babel-parser/src/tokenizer/index.js 中与标识符识别相关的方法是:
readWord(firstCode: number | void): void {
const word = this.readWord1(firstCode);
const type = keywordTypes.get(word) || tt.name;
this.finishToken(type, word);
},
readWord1(firstCode: numbder | void): string {
...
}
2
3
4
5
6
7
8
# 数字直接量: num
JavaScript 中表示数字的方式
字面量
使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法: 十进制、十六进制、八进制、二进制。
十进制: 没有前导
0
的数值。只有十进制可以带小数和用科学计数法表示。
带小数时小数点前后部分都可以省略(不能同时省略),如
0.1 / 1. / 2.4
。八进制: 有前缀
0o
或0O
的数值,或者有前导0
、且只用到0-7
的八个阿拉伯数字的数值。十六进制: 有前缀
0x
或0X
的数值。二进制: 有前缀
0b
或0B
的数值。
科学计数法
科学计数法表示允许字母
e
或E
的后面,跟着一个整数,表示这个数值的指数部分。123e3 // 123000 123e-3 // 0.123 -3.1E+12 .1e-23
1
2
3
4特殊数值
正零和负零
在 JavaScript 的 64 位浮点数中,有一个二进制位是符号位,那么,0 也是有一个对应的负值的。
+0
和-0
都指的是0
,只是它们的64位浮点数表示法的符号位不同。一般状态下,
+0
和-0
是等价的,会被当做正常的0
:+0 // 0 -0 // 0 (-0).toString() // '0' (+0).toString() // '0'
1
2
3
4只是,如果
+0
和-0
作为分母,则有一些区别:(1 / +0) === (1 / -0) // false
1因为
1 / +0
的结果是+Infinity
,1 / -0
的结果是-Infinity
。NaN
Infinity
文件 babel/packages/babel-parser/src/tokenizer/index.js 对数字的处理
readInt()
TODO
readRadixNumber()
TODO
readNumber()
TODO
# 正则表达式直接量: regexp
正则的
flag
正则有如下
flag
:g/m/s/i/y/u/d
charCodes.lowercaseG charCodes.lowercaseM charCodes.lowercaseS charCodes.lowercaseI charCodes.lowercaseY charCodes.lowercaseU charCodes.lowercaseD
1
2
3
4
5
6
7tokenizer 对正则的处理
readRegexp(): void { const start = this.state.start + 1; let escaped, inClass; let { pos } = this.state; for (; ; ++pos) { if (pos >= this.length) { throw this.raise(start, Errors.UnterminatedRegExp); } const ch = this.input.charCodeAt(pos); if (isNewLine(ch)) { // 遇到换行符,抛出错误,遇到未完结的正则表达式 throw this.raise(start, Errors.UnterminatedRegExp); } if (escaped) { escaped = false; } else { if (ch === charCodes.leftSquareBracket) { // [ inClass = true; } else if (ch === charCodes.rightSquareBracket && inClass) { // ] inClass = false; } else if (ch === charCodes.slash && !inClass) { // / break; } escaped = ch === charCodes.backslash; // \ } } const content = this.input.slice(start, pos); // 内容 ++pos; let mods = ""; while (pos < this.length) { const cp = this.codePointAtPos(pos); // It doesn't matter if cp > 0xffff, the loop will either throw or break because we check on cp const char = String.fromCharCode(cp); if (VALID_REGEX_FLAGS.has(cp)) { if (mods.includes(char)) { this.raise(pos + 1, Errors.DuplicateRegExpFlags); // 重复的 flag } } else if (isIdentifierChar(cp) || cp === charCodes.backslash) { this.raise(pos + 1, Errors.MalformedRegExpFlags); // 非法正则 } else { break; } ++pos; mods += char; } this.state.pos = pos; this.finishToken(tt.regexp, { pattern: content, flags: mods, }); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 字符串直接量: string
readString(quote: number): void {
let out = "",
chunkStart = ++this.state.pos;
for (;;) {
if (this.state.pos >= this.length) {
throw this.raise(this.state.start, Errors.UnterminatedString);
}
const ch = this.input.charCodeAt(this.state.pos);
if (ch === quote) break;
if (ch === charCodes.backslash) {
out += this.input.slice(chunkStart, this.state.pos);
// $FlowFixMe
out += this.readEscapedChar(false);
chunkStart = this.state.pos;
} else if (
ch === charCodes.lineSeparator ||
ch === charCodes.paragraphSeparator
) {
++this.state.pos;
++this.state.curLine;
this.state.lineStart = this.state.pos;
} else if (isNewLine(ch)) {
throw this.raise(this.state.start, Errors.UnterminatedString);
} else {
++this.state.pos;
}
}
out += this.input.slice(chunkStart, this.state.pos++);
this.finishToken(tt.string, out);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 模板字符串: template
模板字符串是允许嵌入表达式的字符串字面量,可以使用多行、表达式引入、标签引入功能。
它有如下形态:
// 单行
`string text`
// 多行
`string text line 1
string text line 2`
// 表达式引入
`string text ${expression} string text`
// 标签引入
tag`string text ${expression} string text`
2
3
4
5
6
7
8
9
10
11
12
token 解析时,需要对以下情况做处理。
- 该代码段第一批字符是 ` 开头的话,会记录为一个
tt.backQuote
类型 - 该代码段第一批字符是
${
开头的话,会记录为一个tt.dollarBraceL
类型 - 在扫描该代码段途中,遇到转译标志
\
、换行\r
及\n
时,会继续扫描检查是否满足对目标格式的要求
# 符号
JavaScript 中有如下符号:
{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= => / /= }
因需识别字符串模板,
{
和}
也被视为可能有意义的符号 因需识别除法和正则,/
也被视为可能有意义的符号
.
:dot
当源码遍历过程中,遇到
.
字符时,会调用readToken_dot
方法。.
开头的合法 token 的值有两种: 数字(如.123
)、扩展运算符(...
)。readToken_dot(): void { const next = this.input.charCodeAt(this.state.pos + 1); // 识别 ".123" if (next >= charCodes.digit0 && next <= charCodes.digit9) { this.readNumber(true); return; } // 识别 "..." if (next === charCodes.dot && this.input.charCodeAt(this.state.pos + 2) === charCodes.dot) { this.state.pos += 3; this.finishToken(tt.ellipsis); } else { ++this.state.pos; this.finishToken(tt.dot); } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/
:slash
当源码遍历过程中,遇到
/
字符时,会调用readToken_slash
方法。readToken_slash(): void { const next = this.input.charCodeAt(this.state.pos + 1); if (next === charCodes.equalsTo) { // /= this.finishOp(tt.slashAssign, 2); } else { this.finishOp(tt.slash, 1); } }
1
2
3
4
5
6
7
8该方法可以识别2种token:
/=
、/
。*
:star
或%
:modulo
当源码遍历过程中,遇到
*
或%
字符时,会调用readToken_mult_modulo
方法。readToken_mult_modulo(code: number): void { // '%*' let type = code === charCodes.asterisk ? tt.star : tt.modulo; // asterisk: * let width = 1; let next = this.input.charCodeAt(this.state.pos + 1); // Exponentiation operator ** if (code === charCodes.asterisk && next === charCodes.asterisk) { width++; next = this.input.charCodeAt(this.state.pos + 2); type = tt.exponent; // ** } if (next === charCodes.equalsTo && !this.state.inType) { width++; type = tt.assign; // *= / %= } this.finishOp(type, width); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20该方法可以识别 3 种 token:
**
、*=
、%=
。|
或&
当源码遍历过程中,遇到
|
或&
字符时,会调用readToken_pipe_amp
方法。该方法可以识别 3 种 token:
||
、、&&
、、||=
、、&&=
、|>
、|}
、|]
。值得注意的是,该方法内部有对部分插件的引入要求:
// '|}' if (this.hasPlugin("recordAndTuple") && next === charCodes.rightCurlyBrace) { // 要求引入插件 babel-plugin-syntax-record-and-tuple, // 并且插件的 syntaxType 配置项的值不能是 "bar" if (this.getPluginOption("recordAndTuple", "syntaxType") !== "bar") { // 没有引入插件,抛出 throw this.raise( this.state.pos, Errors.RecordExpressionBarIncorrectEndSyntaxType, ); } ... }
1
2
3
4
5
6
7
8
9
10
11
12
13
14^
当源码遍历过程中,遇到
^
字符时,会调用readToken_caret
方法。readToken_caret(): void { const next = this.input.charCodeAt(this.state.pos + 1); if (next === charCodes.equalsTo) { // ^= this.finishOp(tt.assign, 2); } else { this.finishOp(tt.bitwiseXOR, 1); // 异或运算符 } }
1
2
3
4
5
6
7
8该方法可以识别 2 种 token:
^=
、^
。+
或-
当源码遍历过程中,遇到
+
或-
字符时,会调用readToken_plus_min
方法。readToken_plus_min(code: number): void { // '+-' const next = this.input.charCodeAt(this.state.pos + 1); if (next === code) { this.finishOp(tt.incDec, 2); // ++ -- return; } if (next === charCodes.equalsTo) { // += -= this.finishOp(tt.assign, 2); } else { this.finishOp(tt.plusMin, 1); // + - } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15该方法可以识别6种token:
++
、--
、+=
、-=
、+
、-
。<
或>
当源码遍历过程中,遇到
<
或>
字符时,会调用readToken_lt_gt
方法。readToken_lt_gt(code: number): void { // '<>' const next = this.input.charCodeAt(this.state.pos + 1); let size = 1; if (next === code) { size = code === charCodes.greaterThan && this.input.charCodeAt(this.state.pos + 2) === charCodes.greaterThan ? 3 : 2; if (this.input.charCodeAt(this.state.pos + size) === charCodes.equalsTo) { this.finishOp(tt.assign, size + 1); return; } this.finishOp(tt.bitShift, size); // << >> return; } if (next === charCodes.equalsTo) { // <= | >= size = 2; } this.finishOp(tt.relational, size); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27readToken_lt_gt
方法可以识别 6 种 token:<<
、>>
、<
、>
、<=
、>=
。=
或!
当源码遍历过程中,遇到
=
或!
时,会调用readToken_eq_excl
方法。readToken_eq_excl(code: number): void { // = ! const next = this.input.charCodeAt(this.state.pos + 1); if (next === charCodes.equalsTo) { // == != this.finishOp( tt.equality, this.input.charCodeAt(this.state.pos + 2) === charCodes.equalsTo ? 3 : 2, ); // === !== == != return; } if (code === charCodes.equalsTo && next === charCodes.greaterThan) { // '=>' this.state.pos += 2; this.finishToken(tt.arrow); return; } this.finishOp(code === charCodes.equalsTo ? tt.eq : tt.bang, 1); }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18readToken_lt_gt
方法可以识别7种token:=
、!
、==
、!=
、===
、!==
、==>
。?
当源码遍历过程中,遇到
?
,会调用readToken_eq_excl
方法。readToken_question(): void { const next = this.input.charCodeAt(this.state.pos + 1); const next2 = this.input.charCodeAt(this.state.pos + 2); if (next === charCodes.questionMark) { // ?? if (next2 === charCodes.equalsTo) { // ??= this.finishOp(tt.assign, 3); } else { this.finishOp(tt.nullishCoalescing, 2); // ?? } } else if ( next === charCodes.dot && !(next2 >= charCodes.digit0 && next2 <= charCodes.digit9) // ?. ) { // '.' not followed by a number this.state.pos += 2; this.finishToken(tt.questionDot); } else { ++this.state.pos; this.finishToken(tt.question); // ? } }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21readToken_lt_gt
方法可以识别 4 种 token:?
、??
、??=
、?.
。
# 词法错误
词法错误发生的时机
Tokenizer
使用继承自babel/packages/babel-parser/src/parser/error.js
的raise()
方法抛出词法分析中扫描到的错误。Tokenizer
中抛出词法错误的时机有如下几种场景:严格模式中发现错误
严格模式(
"use strict;"
)下有一系列的限制条件,Tokenizer
在分析过程中,会尝试发现这些错误并抛出。- 禁止出现
0
开头的数字,如"use strict"; 010;
,在词法分析阶段就会发现并抛出该错误,但允许\\0
开头的数字 - 禁止出现八进制数字
- 禁止出现
扫描到的疑似词法单元不满足格式要求
比如块级注释
/* */
缺乏闭合标签、hashbang 注释#!
匹配到了不合适的字符如#2
、正则表达式缺乏闭合标签等。缺乏必要的 Babel 插件引入
对于某些特殊词法单元,需要显示引入对应的插件才会开启支持,否则会抛出错误。
Tokenizer
在解析时,出现过多次对recordAndTuple
插件的引入要求。#{
语法要求引入recordAndTuple
插件且syntaxType
配置项设置为hash
{|
和|}
语法要求引入recordAndTuple
插件且syntaxType
配置项设置为bar
未知的词法单元,也就是常见的
UnexpectedToken
如果 Babel 定义的词法单元都没有被识别出的话,就会抛出
UnexpectedToken
类型的错误,文案描述为"Unexpected character ..."
。比如,源码中出现了一个不属于任何词法单元类型的字符时,会抛出
UnexpectedToken
的错误:const · = 1;
1在词法解析时就会抛出错误:
Unexpected character '·'.
,因为·
并不是一个合法的词法单元字符。
词法错误列表
在词法分析时,是能够发现源码中的一些问题的,比如括号缺少闭合标签等。
这里列举了 Babel 在词法分析阶段能够发现的错误:
错误描述定义在文件
babel/packages/babel-parser/src/parser/error-message.js
RecordExpressionBarIncorrectStartSyntaxType
Record expressions starting with '{|' are only allowed when the 'syntaxType' option of the 'recordAndTuple' plugin is set to 'bar'.
1{|
语法要求引入recordAndTuple
插件且syntaxType
配置项设置为bar
。InvalidOrUnexpectedToken
Unexpected character '%0'.
1出现了非法的 token 字符。
UnterminatedRegExp
Unterminated regular expression.
1未闭合的正则表达式。
DuplicateRegExpFlags
Duplicate regular expression flag.
1重复的正则 flag。
MalformedRegExpFlags
Invalid regular expression flag.
1非法的正则 flag
UnexpectedNumericSeparator
A numeric separator is only allowed between two digits.
11_a
1SyntaxError: unknown: A numeric separator is only allowed between two digits. (1:1) > 1 | 1_a | ^
1
2
3
4NumericSeparatorInEscapeSequence
Numeric separators are not allowed inside unicode escape sequences or hex escape sequences.
1InvalidDigit
Expected number in radix %0.
1InvalidDecimal
Invalid decimal.
1NumberIdentifier
Identifier directly after number.
1无效的小数。
InvalidNumber
Invalid number.
1非法数字。
ZeroDigitNumericSeparator
Numeric separator can not be used after leading 0.
1InvalidOrMissingExponent
Floating-point numbers require a valid exponent after the 'e'.
1InvalidBigIntLiteral
Invalid BigIntLiteral.
1非法的大数字面量。
InvalidCodePoint
Code point out of bounds.
1UnterminatedString
Unterminated string constant.
1未闭合的字符串。
InvalidEscapeSequence
Bad character escape sequence.
1MissingUnicodeEscape
Expecting Unicode escape sequence \\uXXXX.
1EscapedCharNotAnIdentifier
Invalid Unicode escape.
1InvalidEscapedReservedWord
Escape sequence in keyword %0.
1