JavaScript词法
InputElement 输入元素
输入元素是JS词法扫描程序拿到的最基本元素了,也就是JS程序源代码中表达特定意义的"单词"。
输入元素共分为四种:
InputElement :: WhiteSpace Comment Token LineTerminator
值得注意的是,JS规范里面其实定义了两种InputElement ,如下所示
InputElementDiv :: WhiteSpace Comment Token LineTerminator DivPunctuator
InputElementRegExp :: WhiteSpace Comment Token LineTerminator RegularExpressionLiteral
这么做是因为JS的除法运算符和正则表达式直接量都使用了/字符,在词法分析阶段,是无法区分二者的。所以JavaScript的词法分析有两种状态,一种状态是扫描InputElementDiv,另一种状态是扫描InputElementRegExp,又所以,JS的词法分析器应该有两种状态,由语法分析器来设置,JavaScript的词法分析和语法分析必须交错进行。
下面的一个例子说明了除法和正则表达式写法的冲突问题:
if(a+b)/a/g;
(a+b)/a/g;
可以看到完全相同的/a/g(而且前面一段字符也相同),可能被理解为除法或者正则表达式。因为必须区分所处的语法环境,所以单单靠词法分析无论如何也无法决定该用除法还是正则表达式来理解。
因为基本上没有任何编辑环境会对文本做语法分析,这个问题也造成了很多语法着色系统无法很好地处理JS正则表达式。
以非语言实现者的角度,完全应该按照最上面一段产生式去理解JS的词法。
WhiteSpace空白符
这个词相信不用细说,所有JS程序员都比较熟悉。JavaScript接受5种ASCII字符为空白符,BOM以及Unicode分类中所有属于whitespace分类的字符也可以作为空白符使用:
WhiteSpace :: <TAB> <VT> <FF> <SP> <NBSP> <BOM> <USP>
其中,<TAB>是U+0009,是缩进TAB符,也就是字符串中写的'\t'。
<VT>是U+000B,也就是垂直方向的TAB符'\v',这个字符在键盘上很难打出来,所以很少用到。
<FF>是U+000C,Form Feed,分页符,字符串直接量中写作'\f',现代已经很少有打印源程序的事情发生了,所以这个字符在JS源代码中很少用到。
<SP>是U+0020,就是最普通的空格了。
<NBSP>是U+00A0,非断行空格,它是SP的一个变体,在文字排版中,可以避免因为空格在此处发生断行,其它方面和普通空格完全一样。多数的JS编辑环境都会把它当做普通空格(因为一般源代码编辑环境根本就不会自动折行……)
<BOM>是U+FEFF,这是ES5新加入的空白符,是Unicode中的零宽非断行空格,在以UTF格式编码的文件中,常常在文件首插入一个额外的U+FEFF,解析UTF文件的程序可以根据U+FEFF的表示方法猜测文件采用哪种UTF编码方式。这个字符也叫做"bit order mark"。
<USP>表示Unicode中所有的"separator, space(Zs)"分类中的字符,包括:
字符 | 名称 | 你浏览器中的显示 |
---|---|---|
U+0020 | SPACE | |
U+00A0 | NO-BREAK SPACE |
|
U+1680 | OGHAM SPACE MARK |
|
U+180E | MONGOLIAN VOWEL SEPARATOR |
|
U+2000 | EN QUAD |
|
U+2001 | EM QUAD |
|
U+2002 | EN SPACE |
|
U+2003 | EM SPACE |
|
U+2004 | THREE-PER-EM SPACE |
|
U+2005 | FOUR-PER-EM SPACE |
|
U+2006 | SIX-PER-EM SPACE |
|
U+2007 | FIGURE SPACE |
|
U+2008 | PUNCTUATION SPACE |
|
U+2009 | THIN SPACE |
|
U+200A | HAIR SPACE |
|
U+202F | NARROW NO-BREAK SPACE |
|
U+205F | MEDIUM MATHEMATICAL SPACE |
|
U+3000 | IDEOGRAPHIC SPACE |
|
注意虽然JS规范承认这些字符可以被用做空白字符,但是除非对源代码的打印、排版有特别的需求,还是应该尽量使用<SP>,尤其是考虑到,相当一批字体无法支持<USP>中的全部字符。
根据一些团队的编码规范,<TAB>常常用于缩进。编程语言中关于用<TAB>还是四个<SP>做缩进的争论从未停止过,此处就不加讨论了。
JS中,WhiteSpace的大部分用途是分隔token和保持代码整齐美观,基本上词法分析器产生的WhiteSpace都会被语法分析器直接丢弃。
所以一些WhiteSpace能够被去掉而完全不影响程序的执行效果。但是也有一些WhiteSpace必须存在的情况,考虑下面代码:
1 .toString();
1.toString(); //报错
上面一段代码中,空白符分隔了1和.,因此它们被理解为两个token。
1.["toString"]();
1 .["toString"](); //报错
相反的情况。
LineTerminator行终结符
这个也是一个非常常见的概念了,JS中只提供了4种字符作为换行符:
LineTerminator :: <LF> <CR> <LS> <PS>
其中,<LF>是U+000A,就是最正常换行符,在字符串中的'\n'。
<CR>是U+000D,这个字符真正意义上的"回车",在字符串中是'\r',在一部分Windows风格文本编辑器中,换行是两个字符\r\n。
<LS>是U+2028,是Unicode中的行分隔符。
<PS>是U+2029,是Unicode中的段落分隔符。
大部分LineTerminator在被词法分析器扫描出之后,会被语法分析器丢弃,但是换行符会影响JS的两个重要语法特性:自动插入分号和"no line terminator"规则。
考虑下面三段代码:
var a = 1 , b = 1; a ++ b
按照JS语法的自动插入分号规则,代码解释可能产生歧义。
但是因为后自增运算符有no line terminator的限制,所以实际结果等价于:
var a = 1 , b = 1; a; ++b;
考虑以下两段代码:
return 123;
return 123;
因为return有no line terminator的限制,所以第一段代码实际等同于
return; 123;
Comment注释
JS的注释分为单行注释和多行注释两种:
Comment :: MultiLineComment SingleLineComment
多行注释定义如下:
MultiLineComment :: /* MultiLineCommentCharsopt */ MultiLineCommentChars :: MultiLineNotAsteriskChar MultiLineCommentCharsopt * PostAsteriskCommentCharsopt PostAsteriskCommentChars :: MultiLineNotForwardSlashOrAsteriskChar MultiLineCommentCharsopt * PostAsteriskCommentCharsopt MultiLineNotAsteriskChar :: SourceCharacter but not asterisk * MultiLineNotForwardSlashOrAsteriskChar :: SourceCharacter but not forward-slash / orasterisk *
这个定义略微有些复杂,实际上这就是我们所熟知的JS多行注释语法的严格描述。
多行注释中允许自由地出现MultiLineNotAsteriskChar ,也就是除了*之外的所有字符。而每一个*之后,不能出现正斜杠符/
单行注释则比较简单:
SingleLineComment :: // SingleLineCommentCharsopt SingleLineCommentChars :: SingleLineCommentChar SingleLineCommentCharsopt SingleLineCommentChar :: SourceCharacter but not LineTerminator
除了四种LineTerminator之外,所有字符都可以作为单行注释。
一般情况下,不论是单行还是多行注释都不会影响程序的意义,但是含有LineTerminator的多行注释会影响到自动插入分号规则:
return/* */123;
return /**/ 123;
两者会产生不同的效果。
Token
Token是JS中所有能被引擎理解的最小语义单元。
JS中有4种Token:
Token :: IdentifierName Punctuator NumericLiteral StringLiteral
如果不考虑除法和正则的冲突问题,Token还应该包括RegularExpressionLiteral,而Punctuator中也应该添加 / 和 /=两种符号。
IdentifierName
IdentifierName的定义为:
IdentifierName :: IdentifierStart IdentifierName IdentifierPart IdentifierStart :: UnicodeLetter $ _ \ UnicodeEscapeSequence IdentifierPart :: IdentifierStart UnicodeCombiningMark UnicodeDigit UnicodeConnectorPunctuation <ZWNJ> <ZWJ>
IdentifierName可以以美元符$下划线_ 或者Unicode字母开始,除了开始字符以外,IdentifierName中还可以使用Unicode中的连接标记、数字、以及连接符号。
IdentifierName的任意字符可以使用JS的Unicode转义写法,使用Unicode转义写法时,没有任何字符限制。
IdentifierName可以是Identifier、NullLiteral、BooleanLiteral或者keyword,在ObjectLiteral中,IdentifierName还可以被直接当做属性名称使用。
仅当不是保留字的时候,IdentifierName会被解析为Identifier。
UnicodeLetter, UnicodeCombiningMark, UnicodeDigit, UnicodeConnectorPunctuation各自对应几个Unicode的分类。
JS词法名 | Unicode分类名 | code | 字符数 |
---|---|---|---|
UnicodeLetter | Uppercase letter | Lu | 1441 |
Lowercase letter | Ll | 1751 | |
Titlecase letter | Lt | 31 | |
Modifier letter | Lm | 237 | |
Other letter | Lo | 11788 | |
Letter number | Nl | 224 | |
UnicodeCombiningMark | Non-spacing mark | Mn | 1280 |
Combining spacing mark | Mc | 353 | |
UnicodeDigit | Decimal number | Nd | 460 |
UnicodeConnectorPunctuation | Connector punctuation | Pc | 10 |
注意<ZWNJ>和<ZWJ>是ES5新加入的两个格式控制字符,但是目前为止实测还没有浏览器支持。
JS中的关键字有:
Keyword :: one of break do instanceof typeof case else new var catch finally return void continue for switch while debugger function this with default if throw delete in try
还有7个为了未来使用而保留的关键字:
FutureReservedKeyword :: one of class enum extends super const export import
在严格模式下,有一些额外的为未来使用而保留的关键字:
implements let private public interface package protected static yield
除了这些之外,NullLiteral:
NullLiteral :: null
和BooleanLiteral:
BooleanLiteral :: true false
也是保留字,不能用于Identifier。
Punctuator
JavaScript使用48个运算符,因为前面提到的除法和正则问题, /和/=两个运算符被拆分为DivPunctuator。其余的运算符为:
Punctuator :: one of { } ( ) [ ] . ; , < > <= >= == != === !== + - * % ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= <<= >>= >>>= &= |= ^=
所有运算符在语法分析器中作为不同的symbol出现。
NumericLiteral
JS规范中规定的数字直接量可以支持两种写法:十进制和十六进制整数,尽管标准中没有提到,但是大部分JS实现还支持以0开头的八进制整数写法。
所以实际上JS的NumericLiteral产生式应该是这样的:
NumericLiteral :: DecimalLiteral HexIntegerLiteral OctalIntegerLiteralnot-standard
只有十进制可以表示浮点数,DecimalLiteral 定义如下:
DecimalLiteral :: DecimalIntegerLiteral . DecimalDigitsopt ExponentPartopt . DecimalDigits ExponentPartopt DecimalIntegerLiteral ExponentPartopt DecimalIntegerLiteral :: 0 NonZeroDigit DecimalDigitsopt DecimalDigits :: DecimalDigit DecimalDigits DecimalDigit DecimalDigit :: one of 0 1 2 3 4 5 6 7 8 9 NonZeroDigit:: one of 1 2 3 4 5 6 7 8 9 ExponentPart:: ExponentIndicator SignedInteger ExponentIndicator :: one of e E SignedInteger :: DecimalDigits + DecimalDigits - DecimalDigits
JS中的StringLiteral支持单引号和双引号两种写法。
十进制数的小数点前和小数点后均可以省略, 所以 1. 和 .1 都是合法的数字直接量,特别地,除了0之外,十进制数不能以0开头(这其实是为了八进制整数预留的)。
.同时还是一个Punctuator,在词法分析阶段,.123 应该优先被尝试理解为 NumericLiteral ,而非 Punctuator NumericLiteral。
十六进制整数产生式如下:
HexIntegerLiteral :: 0x HexDigit 0X HexDigit HexIntegerLiteral HexDigit HexDigit :: one of 0 1 2 3 4 5 6 7 8 9 a b c d e f A B C D E F
JS中支持0x标记的大小写形式,十六进制数字中的大小写也可以任意使用。
八进制整数是非标准的,但是大多数引擎都支持:
OctalIntegerLiteral :: 0 OctalDigit OctalIntegerLiteral OctalDigit OctalDigit :: one of 0 1 2 3 4 5 6 7
StringLiteral
JS中的StringLiteral支持单引号和双引号两种写法。
StringLiteral :: " DoubleStringCharactersopt " ' SingleStringCharactersopt '
单双引号的区别仅仅在于写法,在双引号字符串直接量中,双引号必须转义,在单引号字符串直接量中,单引号必须转义
DoubleStringCharacters :: DoubleStringCharacter DoubleStringCharactersopt SingleStringCharacters :: SingleStringCharacter SingleStringCharactersopt DoubleStringCharacter :: SourceCharacter but not double-quote " or backslash \ or LineTerminator \ EscapeSequence LineContinuation SingleStringCharacter :: SourceCharacter but not single-quote ' orbackslash \ or LineTerminator \ EscapeSequence LineContinuation
字符串中其他必须转义的字符是\和所有换行符。
JS中支持四种转义形式,还有一种虽然标准没有定义,但是大部分实现都支持的八进制转义
EscapeSequence :: CharacterEscapeSequence 0 [lookahead no DecimalDigit] HexEscapeSequence UnicodeEscapeSequence OctalEscapeSequencenot-standard
第一种是单字符转义。 即一个反斜杠\ 后面跟一个字符这种形式。
CharacterEscapeSequence :: SingleEscapeCharacter NonEscapeCharacter SingleEscapeCharacter :: one of ' " \ b f n r t v NonEscapeCharacter :: SourceCharacter but notEscapeCharacter or LineTerminator
有特别意义的字符包括有SingleEscapeCharacter所定义的9种,见下表:
转义字符 | 转义结果 | 你浏览器中的显示 |
---|---|---|
' | U+0022 | " |
" | U+0027 |
' |
\ | U+005C |
\ |
b | U+0008 | |
f | U+000C | |
n | U+000A | |
r | U+000D | |
t | U+0009 |
|
v | U+000B |
除了这9种字符、数字、x和u以及所有的换行符之外,其它字符经过\转义都是自身。
十六进制转义只支持两位,也就是说,这种写法只支持ASCII字符:
HexEscapeSequence :: x HexDigit HexDigit
Unicode转义可以支持BMP中的所有字符:
UnicodeEscapeSequence :: u HexDigit HexDigit HexDigit HexDigit
LineContinuation可以被理解为一种特别的转义。写字符串直接量时灵活使用LineContinuation可以增加可读性。
LineContinuation :: \ LineTerminatorSequence LineTerminatorSequence :: <LF> <CR> [lookahead no <LF> ] <LS> <PS> <CR> <CR> <LF>
为了适应Windows风格的文本,JS把"\r\n"作为一个换行符使用。
注意因为CR在某些windows风格的编辑器中没法显示出来,所以乱用的话会产生奇怪的效果。
RegularExpressionLiteral
正则表达式由Body和Flags两部分组成:
RegularExpressionLiteral :: / RegularExpressionBody / RegularExpressionFlags
其中Body部分至少有一个字符,第一个字符不能是*(因为/*跟多行注释有词法冲突。)
RegularExpressionBody :: RegularExpressionFirstChar RegularExpressionChars RegularExpressionChars :: [empty] RegularExpressionChars RegularExpressionChar RegularExpressionFirstChar :: RegularExpressionNonTerminator but not * or \ or / or [ RegularExpressionBackslashSequence RegularExpressionClass RegularExpressionChar :: RegularExpressionNonTerminator but not \ or / or [ RegularExpressionBackslashSequence RegularExpressionClass
除了\ / 和 [ 三个字符之外,JS正则表达式中的字符都是普通字符。
RegularExpressionBackslashSequence :: \ RegularExpressionNonTerminator RegularExpressionNonTerminator :: SourceCharacter but not LineTerminator
用 \和一个非换行符可以组成一个RegularExpressionBackslashSequence,这种方式可以用于表示正则表达式中的有特殊意义的字符。
RegularExpressionClass :: [ RegularExpressionClassChars ]
正则表达式中,用一对方括号表示class。class中的特殊字符仅仅为]和\。
class允许为空。
class中也支持转义。
RegularExpressionClassChars :: [empty] RegularExpressionClassChars RegularExpressionClassChar RegularExpressionClassChar :: RegularExpressionNonTerminator but not ] or \ RegularExpressionBackslashSequence
正则表达式中的flag在词法阶段不会限制字符,虽然只有ig几个是有效的,但是任何IdentifierPart序列在词法阶段都会被认为是合法的。
RegularExpressionFlags :: [empty] RegularExpressionFlags IdentifierPart
一些词法分析认为合法,但是实际上不符合正则语法的例子:
附表 JS词法摘要
英文名 | 名称 | 简述 | 示例 |
---|---|---|---|
InputElement | 输入元素 | 一切JS中合法的"词" | |
┣Comments | 注释 | 用于帮助阅读的文本 | |
┃┣SingleLineComments | 单行注释 | 以//起始的单行注释 |
//I'm comments |
┃┗MultiLineComments | 多行注释 | 以/*起始以*/结束的注释 |
/*I'm comments,too.*/ |
┣WhiteSpace | 空白 | 起到分隔或者保持美观作用的空白字符 |
|
┣Token | 词法标记 | 一切JS中有实际意义的词法标记 | |
┃┣IdentifierName | 标识名称 | 以字母或_或$开始的一个单词,可以用于属性名 | |
┃┃┣Identifier | 标识符 | 非保留字的IdentifierName,可以用于变量名或者属性名 |
abc |
┃┃┣Keyword | 关键字 | 有特殊语法意义的IdentifierName |
while |
┃┃┣NullLiteral | Null直接量 | 表示一个Null类型的值 |
null |
┃┃┗BooleanLiteral | 布尔直接量 | 表示一个Boolean类型的值 |
true |
┃┣Punctuator | 标点符号 | 表示特殊意义的标点符号 |
* |
┃┣NumericLiteral | 数字直接量 | 表示一个Number类型的值 |
.12e-10 |
┃┣StringLiteral | 字符串直接量 | 表示一个String类型的值 |
"Hello world!" |
┃┗RegularExpressionLiteral | 正则表达式直接量 | 表示一个RegularExpression类的对象 |
/[a-z]+$$/g |
┗LineTerminator | 行终结符 | 起到分隔或者保持美观作用的换行字符,可能会影响自动插入分号 |
附表 所有JS词法中的不可见字符
简写 | 字符 | 概述 |
---|---|---|
<TAB> | U+0009 | tab符,用于空白 |
<VT> | U+000B | 竖向tab符,用于空白 |
<FF> | U+000C | 换页,用于空白 |
<SP> | U+0020 | 空格,用于空白 |
<NBSP> | U+00A0 | 非断行空格,用于空白 |
<BOM> | U+FEFF | 零宽非断行空格,字节序标记,用于空白 |
<ZWNJ> | U+200C | 零宽非连接符,用于标识符 |
<ZWJ> | U+200D | 零宽连接符,用于标识符 |
<LF> | U+000A | 换行,用于行终结符 |
<CR> | U+000D | 回车,用于行终结符 |
<LS> | U+2028 | 行分隔符,用于行终结符 |
<PS> | U+2029 | 页分隔符,用于行终结符 |