# JavaScript 词法
文法是编译原理中对语言写法的一种规定,文法分为词法和语法。
JavaScript 引擎在执行代码时,会先进行词法解析。
词法的最小语义单元是:token,翻译为“标记”或“词”。从字符串到 token 的整个过程是没有结构的,只要符合 token 的规则,就构成 token。词法分析技术上可以使用状态机或者正则表达式来进行。
一般来说,词法设计不会包含冲突。不过,JavaScript 中有一些特别之处:
- 除法和正则表达式冲突问题
同样是斜杠运算符/
,/
和 /=
是除法运算符,但是两个斜杠括起来就是正则表达式 /abc/
- 字符串模板和
}
冲突问题
字符串模板语Hello, ${name}
,理论上,${}
内部可以放任何 JavaScript 代码,但是因为这些代码最后需要以}
结尾,所以,这部分代码不允许出现}
运算符,但是有例外情况:
console.log(`Hello, ${function() {}}`)
# 输入分类
词法分析过程,JavaScript 源码文本会被从左到右扫描,并被转换成一系列输入元素:
- WhiteSpace 空白字符
- LineTerminator 换行符
- Comment 注释
- Token 词
- IdentifierName 标识符名称,例如定义的变量名或关键字
- Punctuator 符号,运算符和大括号等符号
- NumbericLiteral 数字直接量,就是数字
- StringLiteral 字符串直接量,就是直接用单引号或双引号引起来的字符串
- Template 字符串模板,用反引号 ` 括起来的直接量
注:直接量(literal),就是程序中能直接使用的数据值。
# 空白符号 WhiteSpace
空白符提升了源码的可读性,并将 token 区分开。这些符号通常不影响代码的功能。在压缩代码的过程中会移除源码的空白,减少数据传输量。
编码 | 名称 | 缩写 | 说明 | 字符串中写法 |
---|---|---|---|---|
U+0009 | 缩进 TAB 符(制表符) | <HT> 或 <TAB> | 水平制表符 | \t |
U+000B | 垂直方向 TAB 符(垂直制表符) | <VT> | 垂直制表符 | \v |
U+000C | 分页符 | <FF> | 分页符 | \f |
U+0020 | 空格 | <SP> | 空格 | |
U+00A0 | 非断行空格 | <NBSP> | 文字排版中在该空格处不会换行 | HTML 中用 生成的就是它 |
U+200C | 零宽非连接符(ES5) | <ZWNJ> | 放置在一些经常被当成连字的字符之间,用于将他们分别以独立形式展示 | |
U+200D | 零宽连接符(ES5) | <ZWJ> | 放置在一些通常不会被标记为连字的字符之间,用于将这些字符以连字形式展示 | |
U+FEFF | 零宽非断行空格(ES5) | <ZWNBSP> 旧称<BOM> | 在以 UTF 格式编码的文件中,常常在文件首插入一个额外的 U+FEFF,解析 UTF 文件的程序可以根据 U+FEFF 的表示方法猜测文件采用哪种 UTF 编码方式 |
# 换行符 LineTerminator
除了空白符外,换行符也可以提高源码的可读性,不同的是,换行符还可以影响 JavaScript 代码的执行,也会影响自动分号补全的执行。
编码 | 名称 | 缩写 | 说明 | 字符串中写法 |
---|---|---|---|---|
U+000A | 换行符 | <LF> | 正常的换行符 | \n |
U+000D | 回车符 | <CR> | 真正意义上的“回车” | \r |
U+2028 | 行分隔符 | <LS> | ||
U+2029 | 段分隔符 | <PS> |
# 注释 Comment
注释用来在源码中增加提示、笔记、建议、警告等信息,可以帮助阅读和理解源码。在调试时,可以用来将一段代码屏蔽掉。
# 单行注释 single-line comment
使用 //
,会将该行中符号以后的文本都视为注释,除了四种 LineTerminator 之外,所有字符都可以作为单行注释。
# 多行注释 multiple-line comment
使用 /* */
,这种方式更加灵活,可以在单行内使用多行注释,当然可以实现多行的注释,甚至可以用在代码中当做行内注释(可读性会变差,谨慎使用)
多行注释中允许自由地出现 MultiLineNotAsteriskChar ,也就是除了 *
之外的所有字符。除了最后的结束位置,其他的任何 *
之后,不能出现正斜杠 /
。
需要注意:多行注释中是否包含换行符号,会对 JavaScript 语法产生影响,对于 “no line terminator”规则来说,带换行的多行注释与换行符是等效的。
# 标识符名称 IdentifierName
IdentifierName 可以是 Identifier(就是我们自己定义的变量、函数)、NullLiteral(null 直接量)、BooleanLiteral 或者 keyword(关键字),在 ObjectLiteral 中,IdentifierName 还可以直接当做属性名称使用。仅当不是保留字时,IdentifierName 会被解析成 Identifier。
IdentifierName 可以以美元符$
、下划线_
、或者字母开始,除了开始字符以外,IdentifierName 中还可以使用连接标记、数字、以及连接符号。
JavaScript 中的一切都是区分大小写的,即关键字、变量、函数名和所有的标识符(Identifier)都要区分大小写。
# 关键字
关键字属于 IdentifierName ,这些关键字可用于表示控制语句的开始或结束,或者用于执行特定操作等。在 JavaScript 中,关键字有:
- await
- break
- case
- catch
- class
- const
- continue
- debugger
- default
- delete
- do
- else
- export
- extends
- finally
- for
- function
- if
- import
- in
- instanceof
- new
- return
- super
- switch
- this
- throw
- try
- typeof
- var
- void
- while
- with
- yield
为未来使用而保留的关键字
- enum
在严格模式下,还有一些额外的为了未来使用而保留的关键字
- implements
- interface
- package
- private
- protected
- public
- static
# 关键字的使用
事实上关键字(保留字)是仅针对标识符(Identifier)的词法定义而言的,而不是标识符名(IdentifierName)的文法定义,例如下面的例子就不排斥关键字作为标识符名。
a = { import: "test" }
a.import
a["import"]
但是下面的就会报错,函数声明的标识符不能使用关键字
function import() {} // 报错
# 符号 Punctuator
因为前面提到的除法和正则问题, /
和 /=
两个运算符被拆封为 DivPunctuator,因为前面提到的字符串模板问题, }
也被独立拆分。加在一起,所有的符号为:
{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= => / /= }
# 数字直接量 NumbericLiteral
JavaScript 规范中规定的数字直接量可以支持四种写法:十进制数、二进制整数、八进制整数和十六进制整数。
十进制的 Number 可以带小数,小数点前后部分都可以省略,但是不能同时省略。
.01
12.
12.01
12.toString() // 因为 12. 会被当作省略了小数点后面部分的数字,而单独看成一个整体,就会报错
// 可以增加一个空格,让点单独成为一个 token,或者再加一个点。
12 .toString()
12..toString()
十进制数直接量还支持科学计数法,这里的 e 后面部分,只允许使用整数。
10.24E+2
10.24e-2
10.24e2
十进制数直接量可以以 0 开头,但是如果 0 以后的最高位比 8 小,数值将会被认为是八进制,不会报错,但得到的值可能不是期望的。
// 谨慎使用 0 开头的数值:
0888 // 转换为十进制 888
0777 // 转换为八进制 777,十进制 511
当以 0x 0b 0o 开头是,表示特定进制的整数,这几种进制都不支持小数,也不支持科学计数法。
0xFA //十六进制整数
0o73 //八进制整数
0b10000 //二进制整数
# 字符串直接量 StringLiteral
字符串直接量支持单引号和双引号两种写法,区别仅仅是写法不同,在双引号字符串直接量中,双引号必须转义,在单引号字符串直接量中,单引号必须转义。字符串中其他必须转义的字符是 \
和所有的换行符。
JavaScript 中支持四种转义,单字符转义有特别意义的字符包括 SingleEscapeCharacter 定义的 9 中。此外还有数字、x 和 u。
// 十六进制转义序列
'\xA9' //"©"
// Unicode 转义序列
'\u00A9' //"©"
// Unicode 编码转义
'\u{2F804}' //"你"
# 正则表达式直接量 RegularExpressionLiteral
正则表达式由 Body 和 Flags 两部分组成:
/RegularExpressionBody/Flags
其中 Body 部分至少有一个字符,以避免当成是行注释符号。第一个字符不能是 *
,因为 /*
跟多行注释有语法冲突。正则表达式有自己的语法规则,在词法阶段,仅会对它做简单解析。
# 字符串模板
模板就是一个有反引号括起来的,可以在中间插入代码的字符串。模板支持添加处理函数的写法,这时模板的各段会被拆开,传递给函数当参数:
function f(){
console.log(arguments);
}
var a = "world"
var b = "ha"
f`Hello ${a}!${b}~`; //[["hello","!","~"],"word","ha"]
# 带注释的 ECMAScript 5.1 规范 (opens new window)
# 零宽非断行空格,零宽非连接符,零宽连接符的应用
- 隐形水印(隐形指纹)
- 加密信息分享
- 逃脱敏感词过滤
以下几篇文章是详细介绍: