编程语言的词法结构是一套基本的规则,规定了如何使用这门语言编写程序。词法结构是一门语言最低级的语法,规定了变量如何命名、注释的定界符,以及如何分隔程序的语句,等等。这篇文章主要学习 JavaScript 的词法结构。
JavaScript 程序文本
- JavaScript 是区分大小写的
- JavaSript 会忽略程序记号(token)之间的空格。很大程度上 JavaScript 也会忽略换行符(也有例外)。因为可以在程序中随意使用空格和换行,所以可以按照便于理解的方式对程序进行格式化和缩进
- 除了常规空格,JavaScript 将制表符、各种 ASCII 控制字符和 Unicode 间格识别为空格
- JavaScript 将换行符、回车符、回车/换行序列识别为行终止符
注释
JavaScript 支持两种注释方式:
- 单行注释:以
//
到一行末尾的内容都为注释 - 多行注释:位于
/*
和*/
之间的内容都为注释,可以跨行,但是不能嵌套
1 | // comment1 |
字面量
字面量(literal)是一种直接出现在程序中的数据值。以下都是字面量:
1 | 12 |
标识符和保留字
简单来说,标识符就是一个名字。在 JavaScript 中,标识符用于在 JavaScript 代码中命名常量、变量、属性、函数和类,以及为某些循环提供标记(label)。JavaScript 标识符只能由字符、数字、下划线(_)和美元符号($)组成,但不能以数字开头。之所以数字不能用作第一个字符,是便于 JavaScript 区分标识符和数值。
JavaScript 为语言自身使用而保留了部分标识符,这些保留字不能用作常规标识符。最简单的做法就是不要使用这些单词作为标识符,但 from、set 和 target 除外,因为使用它们很安全,也很常见。
Unicode
JavaScript 程序是使用 Unicode 字符集编写的,因此在字符串和注释中可以使用任意 Unicode 字符。考虑到可移植性和易于编辑,建议在标识符中只使用 ASCII 字母和数字(虽然语言本身允许在标识符中使用 Unicode 字母、数字和象形文字,但不支持表情符号)。
1 | let a = 10; |
Unicode 转义序列
有些计算机硬件/软件无法显示、输入或正确处理全部 Unicode 字符。JavaScript 定义了转义序列,可以仅使用 ASCII 字符来表示 Unicode 字符。
- Unicode 转义序列以
\u
开头,后面跟随四位十六进制数或者包含在一对花括号内的 1-6 位十六进制数字 - JavaScript 早期只支持 4 位数字转义序列,带花括号的版本是 ES6 新增的,目的是为了更好地支持大于 16 位的 Unicode 码点(例如表情符号)
- Unicode 转义序列可以出现在 JavaScript 字符串字面量、正则表达式字面量和标识符(不能出现在语言关键字中)
- Unicode 转义序列也可以出现在注释中,但是由于注释会被忽略,所以注释中的转义序列会被当做 ASCII 字符处理,不会被解析为 Unicode
1 | let s1 = "中文"; |
1 | # node |
Unicode 归一化
如果在程序中使用了非 ASCII 字符,那必须知道 Unicode 允许使用多种编码方式表示同一个字符。例如,对于 é
这个 Unicode 字符:
- 可以编码为
\u00E9
- 也可以编码为常规 ASCII 字符 e 后面跟一个重音组合标记
\u0301
这两种编码在文本编辑器中看起来完全相同,但是他们的二进制编码不同,因此 JavaScript 认为它们不同。
Unicode 标准为所有字符定义了首选编码并规定了归一化例程,用于把文本准换为适合比较的规范形式:
- JavaScript 假定自己解释的源代码已经归一化,它自己不会执行任何归一化
- 如果你想在 JavaScript 程序中使用 Unicode 字符,应该保证使用自己的编辑器或其他工具对自己的源代码执行 Unicode 归一化,以防其中包含看起来一样但实际不同的标识符
1 | const caf\u{e9} = 1; |
可选的分号
JavaScript 使用分号 ;
来分割语句。在 JavaScript 中,如果两条语句分别写在两行,通常可以省略它们之间的分号。另外,在程序末尾,如果接下来的记号是右花括号 }
,那么也可以省略分号。
- 很多程序员使用
;
使用分号来明确标识语句结束,即使这些分号不是必须得(本书的风格) - 另一种风格则是尽可能省略分号,只在少数情况下才使用
无论使用哪种情况,都需要了解 JavaScript 中可选分号的细节。
如下两条语句,分别在两行,所以分号可以省略:
1 | a = 3; |
但是如果这样写,则中间必须使用分号:
1 | a = 3; b = 4 |
注意,JavaScript 并非任何时候都把换行符当做分号,而只是在不隐式添加分号就无法解析代码的情况下才这么做。更准确的说,JavaScript 只在下一个非空格字符无法被解释为当前语句的一部分时才把换行符当作分号(存在 3 种例外情况)。
如下代码:
1 | let a |
JavaScript 会将代码解释为:
1 | let a; a = 3; console.log(a); |
- 第一个换行符被当成分号,以为如果没有分号,JavaScript 无法解释代码
let a a
; - 第二个 a 本身可以是一条独立的语句,但是 JavaScript 并没有将第二个换行符当成分号,因为它可以继续解析为更长的语句
a = 3
这些语句终止规则可能导致某些意外情形,如下代码看上去是两条语句:
1 | let y = x + f |
但是 JavaScript 会将解释为:
1 | let y = x + f(a+b).toString(); |
通常如果语句以 (
、[
、/
或 +
、-
开头,就有可能被解释为之前语句的一部分。有的程序员喜欢在所有这种语句前面都防御性地添加一个分号,这样即使它前面的语句被修改,删掉了之前末尾的分号,也不会影响当前语句:
1 | t = 1 |
1 | t = [[1], [2], [3]]; |
1 | t = [[1], [2], [3]]; |
JavaScript 在不能把第二行解析为第一行的连续部分时才把换行符当做分号,存在 3 种例外情况
- 情况 1:对于 return、throw、continue、break 语句,这些语句可以独立存在,也可以后面紧跟标识符或表达式。如果这些单词后面有换行符,JavaScript 会把换行符当做分号
例如:
1 | return |
会被解释为:
1 | return; |
而不是 return true;
所以一定不能再这些关键字和它们后面的表达式之间添加换行符,否则出了这类错误很难调试。
-
情况 2:对于
++
或者--
运算符,既可以放在表达式前面,也可以放在表达式后面。如果想作为后置操作符,则必须与自己操作的表达式位于同一行 -
情况 3:在使用箭头函数时,箭头
=>
必须和参数列表在同一行
另外,实际测试如下实例,JavaScript 会自动将第一个换行符当成分号,因此 a 的值是 3 而不是 34:
1 | a = 3 |
总体来说,省略分号有时候会导致预料之外的情况,而导致难以察觉的 bug。所以总是使用分号来明确标识语句结束,是一个好习惯。