• JavaScript词法(转)


    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符,也就是字符串中写的' '。

    <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,就是最正常换行符,在字符串中的' '。

    <CR>是U+000D,这个字符真正意义上的"回车",在字符串中是' ',在一部分Windows风格文本编辑器中,换行是两个字符 。

    <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 :: 
        /* MultiLineCommentChars
    opt
     */ 
    MultiLineCommentChars :: 
        MultiLineNotAsteriskChar MultiLineCommentChars
    opt
     
        * PostAsteriskCommentChars
    opt
     
    PostAsteriskCommentChars :: 
        MultiLineNotForwardSlashOrAsteriskChar MultiLineCommentChars
    opt
     
        * PostAsteriskCommentChars
    opt
     
    MultiLineNotAsteriskChar :: 
        SourceCharacter but not asterisk * 
    MultiLineNotForwardSlashOrAsteriskChar :: 
        SourceCharacter but not forward-slash / orasterisk *

    这个定义略微有些复杂,实际上这就是我们所熟知的JS多行注释语法的严格描述。

    多行注释中允许自由地出现MultiLineNotAsteriskChar ,也就是除了*之外的所有字符。而每一个*之后,不能出现正斜杠符/

    单行注释则比较简单:

    SingleLineComment ::
        // SingleLineCommentChars
    opt
    SingleLineCommentChars ::
        SingleLineCommentChar SingleLineCommentChars
    opt
    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
        OctalIntegerLiteral
    not-standard

    只有十进制可以表示浮点数,DecimalLiteral 定义如下:

    DecimalLiteral ::
        DecimalIntegerLiteral . DecimalDigits
    opt
     ExponentPart
    opt
        . DecimalDigits ExponentPart
    opt
        DecimalIntegerLiteral ExponentPart
    opt
    DecimalIntegerLiteral ::
        0 
        NonZeroDigit DecimalDigits
    opt
    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 ::
        " DoubleStringCharacters
    opt
     "
        ' SingleStringCharacters
    opt
     '

    单双引号的区别仅仅在于写法,在双引号字符串直接量中,双引号必须转义,在单引号字符串直接量中,单引号必须转义

    DoubleStringCharacters ::
        DoubleStringCharacter DoubleStringCharacters
    opt
    SingleStringCharacters ::
        SingleStringCharacter SingleStringCharacters
    opt
    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
        OctalEscapeSequence
    not-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把" "作为一个换行符使用。

    注意因为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 页分隔符,用于行终结符
     
    好文要顶 关注我 收藏该文  
    10
    0
     
     
     
    « 上一篇: 面向对象闲话(二)——面向对象设计
    » 下一篇: 关于JavaScript词法
    posted @ 2012-04-17 20:22  winter-cn  阅读(6819)  评论(16)  编辑  收藏

     
     
      
    #1楼 2012-04-17 20:42 随风浪迹天涯
    觉得这个好难哦!~。
      
    #2楼 2012-04-17 20:47 日—月
    winter大神
      
    #3楼 2012-04-18 00:45 snadn
    先膜拜winter大神!
    然后
    引用因为return的有no line terminator的限制,所以第一段代码实际等同于
    这句话是不是有错别字呀?
      
    #4楼 2012-04-18 02:09 Franky
    我去,你把整个词法约定部分给总结了啊..

    比我笔记里的词法约定那篇的注解,要给力的多啊.. 顶吧.还能说啥子.
      
    #5楼 [楼主] 2012-04-18 09:57 winter-cn
    @ snadn
    嗯 失误 失误......
      
    #6楼 [楼主] 2012-04-18 09:57 winter-cn
    引用Franky:我去,你把整个词法约定部分给总结了啊..

    比我笔记里的词法约定那篇的注解,要给力的多啊.. 顶吧.还能说啥子.

    效果不好啊 唉
      
    #7楼 2012-04-18 11:34 冰封e族
    这个已经深入到编译的底层了,只有了解规则才能玩的转。
      
    #8楼 2012-04-18 12:44 Franky
    @ winter-cn

    很好啦.要多少是多啊.
      
    #9楼 2012-04-18 21:36 岑安
    大赞啊,收藏
      
    #10楼 2012-04-21 00:37 恋人十八岁
    大神啊。太崇拜了。好希望那天我也能达到这样的高度。努力。
      
    #11楼 2012-05-18 16:36 Hodor
    膜拜winter大神
      
    #12楼 2012-05-29 16:17 AKI______
    1 .toString();
    1.toString();

    区别是1.toString()
    1.是一个数值字面量,NumericLiteral。导致toString不是一个合法的token。
    1 .toString()变成2个token。1 和 .toString()。所以没事。

    词法分析不是非贪婪的。所以1. 变成数值字面量了。
    1..toString()就可以
    或者1.234.toString()。

    不知道这样理解对不对。按理说.是一个token呢? 还是.toString是一个token。.是一个Punctuator
      
    #13楼 2012-09-29 10:30 AKI______
    文中提到space分割token。但是给人的误解就是,不是space就不能分割token的样子。 1+1=2。这个是5个token。
    所以1.toString的问题完全是1. 为一个token。导致后面的出错。
      
    #14楼 2013-12-06 20:33 Jerry Qu
    「一些词法分析认为合法,但是实际上不符合正则语法的例子:」,例子呢?例子呢?
      
    #15楼 [楼主] 2013-12-09 10:16 winter-cn
    @ Jerry Qu
    哈哈 忘记了
    /(0,20)/
      
    #16楼 2014-03-13 17:49 炎燎
    这是我目前看过解释JavaScript词法最通俗易懂的一篇文章,感谢winter大神的贡献。
     
     
  • 相关阅读:
    Linux下查看网卡驱动和版本信息
    HTML <!--...--> 标签
    linux物理网卡检测命令mii-tool
    Git错误non-fast-forward后的冲突解决
    ubuntu上解决访问github慢的方法
    右侧添加悬浮打赏功能
    VSCode 预览 .md 文件
    Matlab绘制三维曲面(以二维高斯函数为例)
    matlab的三维绘图和四维绘图
    ubuntu查看文件和文件夹大小
  • 原文地址:https://www.cnblogs.com/yasepix/p/12222454.html
Copyright © 2020-2023  润新知