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 頁分隔符,用於行終結符


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM