# 语言结构
JavaScript 的语言结构由词法和语法结构组成。
# 词法结构
词法结构是语言的最小单元,包括空白符(White Space)、行终止符(Line Terminators)、注释(Comments)和词(Tokens)。
# 格式控制字符
Unicode 格式控制字符用于控制源文本的显示,但并不会显示出来。
- 零宽非连接符(ZERO WIDTH NON-JOINER,ZWNJ)
用于使用连词的计算机书写系统,例如阿拉伯文字或印度文字和 emoji 等。将其放入两个原本会产生连词的字符之间将不会产生连词效果。Unicode 码点为 U+200C,HTML 实体是 ‌
。
'👨\u200C💻' // => '👨💻' 没有产生连词效果
- 零宽连接符(ZERO WIDTH JOINER,ZWJ)
用于一些复杂文字的计算机排版,例如阿拉伯文字或印度文字和 emoji 等。将其放入两个本来不会连接的字符之间创建一个新的字符。
Unicode 码点为 U+200D 实体是 ‍
。
'👨\u200D💻' // => '👨💻' 产生连词效果
- 字节流方向标识符(BYTE ORDER MARK,BOM)
主要用于文本开头检查文本编码和字节顺序。Unicode 码点为 U+FEFF。
扩展阅读
- 零宽度字符:和谐?屏蔽?不存在的 (opens new window)
- Be careful what you copy: Invisibly inserting usernames into text with Zero-Width Characters (opens new window)
# 空白符
空白符用于在不影响源码功能的前提下将 tokens 分隔从而提升源码的可读性。
- 制表符(CHARACTER TABULATION,TAB)
电脑上使用 TAB 键可以打印该字符,可以用于将文本排版成类似表格的结构。Unicode 码点为 U+0009,转义序列为 \t
。
- 垂直制表符(LINE TABULATION,VT)
Unicode 码点为 U+000B,转义序列为 \v
。
- 分页符(FORM FEED,FF)
Unicode 码点为 U+000C,转义序列为 \f
。
- 空格(SPACE,SP)
平常使用的空格。Unicode 码点为 U+0020。
- 无间断空格(NO-BREAK SPACE,NBSP)
可以使该空格处的两个字符产生不换行的效果。码点为 U+00A0,HTML 实体为
。例如:
可以看到,没有添加无间断空格的 “Java Script” 文本的 “Java” 和 “Script” 两个字符之间出现了换行效果,而添加了无间断空格的文本两字符之间没有出现换行效果。
- 其他空白符
# 行终止符
行终止符用于表示一行文本的结束和新文本的开始,同样可以将 tokens 分隔从而提高源码的可读性,不过行终止符会影响源码的执行,还有自动插入分号规则的执行。
- 换行符(LINE FEED,LF)
Unicode 码点为 U+000A,转义序列是 \n
。
- 回车符(CARRIAGE RETURN,CR)
Unicode 码点为 U+000D,转义序列为 \r
。
- 行分隔符(LINE SEPARATOR,LS)
Unicode 码点为 U+2028。
- 段分隔符(PARAGRAPH SEPARATOR,PS)
Unicode 码点为 U+2029。
TIP
Windows 操作系统继承了电传打字机使用 CR + LF 作为换行符的传统,而类 Unix 操作系统遵循了 Multics 操作系统单独使用 LF 作为换行符的规定。更多换行符的内容,请点击这里 (opens new window)。
# 注释
注释可以为源码提供提示信息,增强源码的可读性,还可以屏蔽指定源码,阻止其执行。
注释包括单行注释和多行注释两种。
// 单行注释
/* 一行内的多行注释 */
/*
* 多行注释(注释内容的 * 字符不是必须的)
*/
其中单行注释以 //
开头,除了行终结符以外的其它源字符都可以作为单行注释的一部分。多行注释以 /*
开头,以 */
结尾,多行注释内容不能嵌套,多行注释可以包含任意源字符,但是 *
和 /
不能组合存在于多行注释中,因为 */
会被视为多行注释的结束标记。
# 词
# 标识符
在计算机科学中,标识符(Identifier)是命名实体(name entities)的词法标记。而在计算机语言中,标识符则是命名语言实体的词法标记。JavaScript 的语言实体包括变量、属性、函数、类和模块等,而标识符用于命名这些实体。
JavaScript 标识符由第一个字符(Start)和后续字符(Part)两部分构成:
第一个字符必须是字母、下划线(_)或者美元符号($);
后续字符可以是字母、下划线、美元符号或数字。
一般情况下,第一个字符以下划线开头的标识符表示私有属性或全局私有变量;而第一个字符以美元符号开头的标识符常用在框架的 API 命名,这样可以避免框架 API 与用户标识符冲突;而不能以数字作为第一个字符为了避免标识符与整数字面量混淆。
WARNING
实际上,JavaSCript 可以使用 Unicode 字符作为标识符,并且后续字符可以包含零宽非连接符和零宽连接符,但不建议使用。
# 保留字
在计算机科学中,保留字(Reserved word)是不能用作标识符的单词,而关键字(Keyword)是特定上下文具有特殊含义的单词,可以在特定上下文用作标识符,关键字是保留字的子集。
JavaScript 的保留字包括:
await break case catch class const continue debugger default delete do else enum export extends false finally for function if implements import in instanceof interface new null package private protected public return super switch this throw true try typeof var void while with yield
yield
虽然是保留字,但可以作为标识符使用。
enum
是未来保留字。implements
interface
package
private
protected
public
在严格模式下作为未来保留字,用于语言未来扩展新特性。
let
static
在严格模式下是保留字。
arguments
eval
虽然即不是保留字也不是关键字,但在严格模式不允许作为标识符使用。
undefined
虽然既不是保留字也不是关键字,但可以在严格模式中作为变量名使用。
保留字的定义会引发一些问题。JavaScript 由于历史原因,保留字规则非常复杂,对于新手来说,不利于记忆,唯一记忆的办法就是不要使用以上单词作为标识符;对于语言发展来说,不利于语言新特性的扩展。
as
async
from
get
of
set
target
是关键字,但不是保留字,可以作为标识符使用。
# 符号
{ ( ) [ ] . ... ; , < > <= >= == != === !== + - * % ** ++ -- << >> >>> & | ^ ! ~ && || ?? ?. ? : = += -= *= %= **= <<= >>= >>>= &= |= ^= &&= ||= ??= => / /= }
其中,值得注意的是 ?.
/
/=
}
符号。
# 字面量
字面量(Literal)是一种在程序中可以直接使用数据值的符号,也称直接量。
# 空值字面量
null
undefined
# 布尔值字面量
true
false
# 数值字面量
- 十进制数字面量
整数
0
24
浮点数
.1 // => 0.1 (不推荐省略前导零)
0.1
3.1415926
以下浮点数将会自动转换为整数:
1. // => 1 (不推荐省略后置零)
1.0 // => 1
1.00 // => 1
科学计数法
科学记数法使用 E
或 e
后跟可选的 + 或 - 操作符后再跟上整数表示非常大或非常小的数(整数或浮点数),E
或 e
后面的整数表示实数值乘以 10 多少次幂。
1e32
1e-32
3.1415E32
3.1415E-32
ES6 增加了二进制和八进制表示法。
- 二进制数字面量
使用 0b
或 0B
表示二进制数,二进制的数值只包含 0 和 1。
0b00011000 // => 24
0B00011000 // => 24
- 八进制数字面量
使用 0o
或 0O
表示八进制数,八进制的数值包括 0 至 7 范围内的整数。
0o030 // => 24
0O030 // => 24
为了方便区分 0 和 大 O,推荐使用 小 o 表示八进制数。
WARNING
在 ES6 以前,可以使用一个前导 0 后面加 0 至 7 组合的整数表示八进制数,而使用上述超出范围的整数表示十进制数,例如 030
返回的数值是八进制的 24 而不是十进制的 30。但在严格模式中,不允许使用前导零来表示二进制和十进制数,使用前导零会被视为语法错误。
- 十六进制数字面量
使用 0x
或 0X
表示十六进制数,十六进制的数值包括 0 至 9 以及 A(或 a)至 F(f),A 至 F 表示 10 至 15。
0x18 // => 24
0x1F // => 31
0x1f // => 31
- 大整数字面量
可以使用以上进制数加后缀 n 表示大整数,但不能使用浮点数和科学计数法表示大整数,因为这样会导致语法错误。
1234567890n // => 1234567890n
0b11110000n // => 240n
0o77777777n // => 16777215n
0xA0B1C2D3n // => 2696004307n
1.24n // => SyntaxError
1e24n // => SyntaxError
- 数值分隔符
ECMA2021 新增了数值分隔符,在数值之间只能使用一个连续的下划线(_
)分隔数值可以提高数值字面量的可读性。数值分隔符可以用在以上所有数值字面量数值部分中间,甚至可以用在浮点数小数部分和科学记数法指数部分数值中间,但是数值前后和数值符号前后不能出现数值分隔符,否则会导致语法错误。
// good code
1_234_567_890 // => 1234567890
0b1111_0000 // => 240
0o77_777_777 // => 16777215
0xA0_B1_C2_D3 // => 2696004307
3.1_415_926 // => 3.1415926
0.12e1_00 // => 1.2e+99
1_000_000_000n // => 1000000000n
// bad code
_1234567890
1234567890_
0_b11110000
0b_11110000
3_.1415926
3._1415926
0.12e_100
1000000000_n
1__000
# 字符串字面量
我们可以使用双引号("")和单引号('')包括零个、一个或多个 Unicode 字符表示字符串字面量,而且当前引号可以嵌套其他引号。
""
"foo"
'1.24'
"I'm front-boy."
'name="front-boy"'
- 转义序列
转义序列使用反斜杠(\)与后面的字符组合,表示在字符串中无法直接使用或输入和拥有其他含义的字符。JavaScript 的转义序列包括字符转义序列、十六进制转义序列和 Unicode 转义序列。
字符转义序列
使用反斜杠后面加上需要转义的字符表示字符转义序列。例如转义换行符、单引号和双引号等字符。
console.log("This is the first line.\nThis is the second line.");
console.log('I\'m front-boy.');
console.log("She said \"hi\", He said.");
十六进制转义序列
使用 \x
开头后面加上两位十六进制数表示 0 至 FF 码点内的字符。
'\xFF' // => ÿ
Unicode 转义序列
使用 \u
开头后面加上四位十六进制数表示 0 至 FFFF 码点内的字符。
'\u2EC1' // => 虎
ES6 增加代理对表示 FFFF 至 10FFFF 码点内的字符。
// ES5
'\ud83d\ude32' // => 😲
// ES6
'\u{1F632}' // => 😲
'\ud83d\ude32' === '\u{1F632}' // => true
以下是所有转义序列:
转义序列 | Unicode | 说明 |
---|---|---|
\0 | \u0000 | NUL 字符 |
\b | \u0008 | 退格符 |
\t | \u0009 | 制表符 |
\n | \u000A | 换行符 |
\v | \u000B | 垂直制表符 |
\f | \u000C | 分页符 |
\r | \u000D | 回车符 |
\" | \u0022 | 双引号 |
\' | \u0027 | 单引号 |
\\ | \u005C | 反斜杠 |
\xnn | \u0000 至 \u00FF | 2 位十六进制 |
\unnnn | \u0000 至 \uFFFF | 4 位十六进制 |
\u{n} | \uFFFF 至 \u10FFFF | 1 至 6 位十六进制 |
最后还有一种鲜为人知的八进制转义序列,例如 '\312'
。
- 模板字面量
使用反引号(``)表示模板字面量。
`template string`
# 正则表达式字面量
正则表达式字面量由一对斜杠(/)包括的模式(pattern)和可选的标志(flags)两个部分构成。例如:
/pattern/flags
模式
模式是由普通字符和元字符(拥有特殊含义的字符)构成的字符串,用于匹配和处理简单或复杂的文本。正则表达式的元字符包括:
. [] + * ? {} ^ $ () / | \ <> ! =
如果需要在模式中使用以上元字符或者具有特殊含义的字符序列,需要使用反斜杠(\)转义。
TIP
因为 \b 在正则表达式中有其他含义,所以退格符需要使用 [\b] 匹配。在正则表达式中,可以使用 \c 开头的前缀指定控制字符 (opens new window)。例如:\cH
可以匹配退格符。不过这种方式很少见。
下面将介绍各元字符的含义及使用场景。
- 单个匹配
单个匹配可以匹配单个字符或者字符集合中任意一个字符。
- 匹配任意字符
.
字符用于匹配的任意单个字符(行终止符除外)。
- 匹配特定字符
[...]
字符用于定义一个字符集合,表示匹配该特定字符集合中的任意一个字符。例如,只匹配数字字符等其它特定或者自定义字符集合,那么可以使用 /[0123456789]/
这种枚举的模式匹配,不过这样表示略显冗余。
-
字符用于定义一个字符区间,常见的字符区间包括数字字符和字母字符区间,这样,我们就可以使用区间模式匹配特定字符集合。例如:
[1-9] // 匹配数字
[a-z] // 匹配小写字母
[A-Z] // 匹配大写字母
如果需要在字符集合中包含连字符,只需在字符集合末尾添加该字符即可。例如 /[A-Za-z0-9-]/
模式可以用于匹配带有字母、数字和连字符。
^
字符用于定义需要排除的字符集合。例如 /[^0-9]/
模式可以排除数字字符,匹配非数字字符。
正则表达式还提供了字符类来更为简单的匹配特定类型的字符。
字符类 | 匹配字符 | 字符集合 |
---|---|---|
\d | 任意一个数字字符 | [0-9] |
\D | 任意一个非数字字符 | [^0-9] |
\w | 任意一个字母数字或下划线字符 | [A-Za-z0-9-] |
\W | 任意一个非字母数字或下划线字符 | [^A-Za-z0-9-] |
\s | 任意 Unicode 空白字符 | [\f\n\r\t\v\u00a0\u1680\u180e\u2000\u200a-\u2028\u2029\u202f\u205f\u3000\ufeff] |
\S | 任意非 Unicode 空白字符 | [^\f\n\r\t\v\u00a0\u1680\u180e\u2000\u200a-\u2028\u2029\u202f\u205f\u3000\ufeff] |
为了匹配更多类型的 Unicode 字符,ES2018 新增了 Unicode 字符类,使用 \p{...}
和 \P{...}
匹配和排除某种 Unicode 字符类型。可以使用以下方式表示 Unicode 字符类:
\p{Unicode 属性名}
\p{Unicode 属性值}
\p{Unicode 属性名 = Unicode 属性值}
例如:
/\p{Number}/ // 匹配所有类型的数字
/\p{White_Space}/ // 匹配所有 Unicode 空白符
/\p{Script=Greek}/ // 匹配希腊文
/[\p{Alphabetic}\p{Decimal_Number}\p{Mark}\p{Connector_Punctuation}\p{Join_Control}]/ // 匹配所有文字字符
更多 Unicode 字符类请点击这里 (opens new window)。
- 重复匹配
重复匹配可以匹配多个连续重复的字符或字符集合。
- 贪婪匹配
字符 | 说明 |
---|---|
+ | 字符或字符集合重复一次或多次 |
* | 字符或字符集合重复零次或多次 |
? | 字符或字符集合重复零次或一次,相当于该字符是可选的 |
{n} | 字符或字符集合重复 n 次 |
{m,n} | 字符或字符集合重复至少 m 次,至多 n 次 |
{n,} | 字符或字符集合重复至少 n 次 |
- 惰性匹配
贪婪匹配会尽可能多的匹配文本,如果想要尽可能少的匹配则需要在以上字符后面添加 ?
,表示惰性匹配文本。例如,/a+?/
模式表示最多只匹配文本的第一个 a 字符。
- 位置匹配
位置匹配可以对文本特定位置进行匹配。
字符 | 说明 |
---|---|
^ | 匹配字符串开始位置 |
$ | 匹配字符串结束位置 |
\b | 匹配单词边界 |
\B | 匹配非单词边界 |
- 子表达式
(...)
元字符用于表示子表达式。在子表达式中,子表达式可以用于任选、分组和反向引用以及断言。子表达式支持嵌套。
- 任选
在子表达式中使用管道符(|
)可以将子表达式分为多个选项,并从左到右依次使用这些选项匹配文本,如果匹配到将不会使用剩余的选项匹配。例如,/(abc|xyz){3}/
模式将匹配 abc 或 xyz 重复 3 次的文本。
- 分组和反向引用
子表达式可以将正则表达式分为多个组,分组将会在匹配文本的同时提供一个捕获分组的引用。如果需要反向引用模式中的分组,可以在模式中使用反斜杠后面紧跟正整数的方式引用分组内容。例如,下面的模式将会引用分组的引号,从而匹配相同的引号:
/(["'`])[^"'`]*\1/
如果我们并不需要捕获分组内容,可以使用在子表达式开头添加 ?:
元字符,表示非捕获组。例如,在 /(?:a)\1/
模式中,反向引用将捕获不到分组中的内容。
为了更直观引用分组而不是使用数字引用,ES2018 新增了具名捕获组特性,在子表达式开头添加 ?<...>
元字符表示具名捕获组,可以通过 \k<...>
元字符反向引用具名捕获组。例如,通过具名捕获组改造引号匹配模式:
/(?<quote>["'`])[^"'`]*\k<quote>/
- 断言
断言用于控制匹配结果返回的文本位置。
向前断言使用 x(?=y)
表示。匹配 x 后面包含 y 文本并返回 x 的匹配结果。例如,/[A-Z][a-zA-Z]+(?=Script)/
模式对于 LiveScript 文本,将匹配包含 Script 的文本并返回 Script 文本前的 Live 文本。
向前否定断言使用 x(?!y)
表示。匹配 x 后面不包含 y 的文本并返回 x 的匹配结果。例如,/Java(?!Script)/
模式匹配 JavaBeans 文本并返回 Java 文本,但并不匹配包含 Script 的 JavaScript 文本。
ES2018 增加了向后断言特性。
向后断言使用 (?<=y)x
表示。匹配 x 前面包含 y 文本并返回 x 的匹配结果。例如,/(?<=\$)\d+/
模式匹配包含美元符号的 $123 文本并返回美元符号后面的 123 数值。
向后否定断言使用 (?<!y)x
表示。匹配 x 前面不包含 y 文本并返回 x 的匹配结果。例如,/\b(?<!\$)\d+\b/
模式匹配文本中不包含美元符号的数值。注意,使用 \b
单词边界可以避免匹配到拥有美元符号后面的数值字符。
标志
标志用于控制文本的搜索模式,标志由一个或多个以下字母表示。
标志 | 说明 |
---|---|
g | 全局搜索模式 |
i | 不区分大小写搜索模式 |
m | 多行搜索模式,适用于多行字符串,^ 和 $ 元字符将匹配每行的开头和结尾 |
u | Unicode 搜索模式(ES6),用于精确的匹配码点及 Unicode 字符类 |
y | 定点搜索模式(ES6) |
s | 任意字符搜索模式(ES2018),模式中使用 . 元字符相当于 [^] |
# 语法结构
# 表达式与操作符
表达式用于从左到右计算求值,类似于自然语言中的短语。表达式由至少一个值和可选的操作符构成。
操作符用于组合运算值或操作表达式,从而形成更复杂的表达式。操作符由特殊符号或关键字构成。根据操作符操作值的数量可以将表达式分为一元、二元和三元表达式。
操作符的优先级 (opens new window)决定了表达式执行的先后顺序,优先级高的操作符先于优先级低的操作符执行。
1 + 2 * 3 // => 7
如上所示,乘法操作符的优先级要比加法操作符的优先级高,所以结果为 7。
操作符的结合性控制了相同优先级的表达式执行的顺序。左结合表示从左往右执行操作,右结合表示从右往左执行操作。右结合的操作符包括带括号的 new
、指数、一元、赋值和三元操作符。
1 ** 2 ** 3 // => 1 相当于 1 ** (2 ** 3)
操作符的副作用会影响表达式求值的结果,有时我们需要的是操作符的副作用而不是操作符的返回值。自增/减、赋值和 delete
操作符具有副作用。
# 基本表达式
基本表达式是 JavaScript 中最简单的表达式。包括部分关键字、标识符引用、字面量。例如:
this // 关键字
name // 标识符引用
true // 布尔值字面量
基本表达式还包括数组字面量、对象字面量和函数表达式。
# 数组字面量
数组字面量是描述初始化数组的表达式,它由方括号包括的以逗号分隔的零个或多个表达式求值构成的元素列表,元素可以是任意表达式。
[]
[1, 2, 3]
ES6 增加了展开语法,从而更简单优雅的将另一个数组的元素浅复制到当前数组。
[1, 2, ...[3, 4]] // => [1, 2, 3, 4]
任意位置的元素都可以省略形成数组空洞,最后一个元素被省略将会被忽略。
[,,] // 只包含两个而不是三个元素
不过由于 ES6 及其之后与 ES6 之前处理数组方法的行为不一致且存在性能隐患,因此在生产中避免使用存在空洞的稀疏数组,而是显示的以 undefined 代替空洞。
通过 ()
分组操作符,可以改变运算符的优先级,在不确定操作符优先级的情况下使用分组操作符可以提高表达式的可读性。
# 左值表达式
左值表达式即可以用于赋值的表达式。包括成员访问表达式、调用表达式和对象创建表达式。
# 成员访问表达式
- 对象成员访问
使用点和方括号表示对象成员访问操作符。使用点访问的属性名必须是合法的标识符,而访问非法标识符的属性名或者计算属性名则需要使用方括号,方括号内的表达式被求值并转换为字符串。
obj.property
obj[expression]
- 父类成员访问
ES6 启用了用于访问父类成员 super
关键字。
super.property
super[expression]
- 元成员访问
ES6 增加了 new.target
属性来判断函数是否被 new
操作符调用。
ES2020 增加了 import.meta
属性获取模块的元信息。
# 调用表达式
在成员访问表达式后面加上括号包裹的零个参数或使用逗号分隔的参数列表构成了调用表达式。调用表达式包括函数和方法调用,父类成员调用以及 ES2020 增加的动态加载模块。
a() // 函数调用
a.b() // 对象方法调用
super() // 对象父类调用
import() // 动态导入
# 对象创建表达式
在成员访问表达式前面加上一个 new
操作符可以构成对象创建表达式,如果没有参数可以省略括号。
new Person
new Person(1)
# 更新表达式
使用前缀或后缀自增/减操作符组合操作数构成了更新表达式。前缀自增/减操作符会更新操作数并返回更新后的值,而后缀自增/减操作符也会更新操作数并返回更新前的值。
// 前缀自增/减
x++ // 相等于 x + 1 => x
x-- // 相等于 x - 1 => x
// 后缀自增/减
++x // 相等于 x + 1 => x + 1
--x // 相等于 x - 1 => x - 1
# 算数表达式
算数表达式用于执行算数求值操作。在加、减、乘、除和求余运算的基础上,ES2016 增加了 **
幂运算符。
+a
-a
a + b
a - b
a * b
a / b
a % b
a ** b
# 位运算表达式
位运算表达式用于将操作数转换为 32 位的二进制整数,然后对操作数进行按位或移位运算后返回整数。
# 位逻辑运算符
- 按位与
按位与(&
)操作符将两个操作数按位做与运算,也就是两个操作数对应的位都为 1 时返回 1,否则返回 0。
2 & 3 // 0010 & 0011 => 0010 => 2
- 按位或
按位或(|
)操作符将两个操作数按位做或运算,也就是两个操作数对应的位有一个为 1 时返回 1,否则返回 0。
2 | 3 // 0010 | 0011 => 0011 => 3
|
可以用于截取操作数的整数部分。
1.1 | 0 // => 1
-1.9 | 0 // => -1
- 按位异或
按位异或(^
)操作符将两个操作数按位做异或运算,也就是两个操作数对应的位不同时返回 1,否则返回 0。
2 ^ 3 // 0010 ^ 0011 => 0001 => 1
- 按位非
按位非(~
)操作符是一个一元操作符,将操作数按位进行非运算,返回操作数的补码,也就是将操作数对应的位取反并减 1。
~2 // => 相当于 -2 - 1 => -(2 + 1) => -3
~~
也可以用来截取操作数的整数部分。
~~1.1 // => 1
~~(-1.9) // => -1
# 移位运算符
- 左移
左移(<<
)操作符将第一个操作数的所有位向左移动第二个操作数指定位数,相当于操作数乘以 2n。左侧移出的位将会被抛弃,右侧空位将用 0 填补。
2 << 1 // 0010 => 0100 => 4
- 右移
右移(>>
)操作符将第一个操作数的所有位向右移动第二个操作数指定位数。右侧移出的位将会被抛弃,左侧空位将用最左侧的符号位填补。
128 >> 4 // 10000000 => 00001000 => 8
-128 >> 4 // 10000000 => 00001000 => -8
- 无符号右移
无符号右移(>>>
)操作符将第一个操作数的所有位向右移动第二个操作数指定位数。右侧移出的位将会被抛弃,左侧空位不管符号位是什么都使用 0 填补。
128 >>> 4 // 10000000 => 1000 => 8
-128 >>> 4 // 11111111111111111111111110000000 => 00001111111111111111111111111000 => 268435448
# 关系表达式
关系表达式用于判断两个操作数的关系,并根据关系返回布尔值。
# 比较操作符
比较操作符针对数值和字符串做比较操作,如果两个操作数是字符串则比较字符编码顺序。
a > b
a < b
a >= b
a <= b
# 相等操作符
宽松相等(==
)与严格相等(===
)操作符用于判断操作数是否相同;宽松不相等(!=
)与严格不相等(!==
)操作符用于判断操作数是否不同。
如果操作数的类型相同,==
与 ===
操作符的行为没有什么区别,否则 ==
操作符需要使用抽象相等比较算法进行隐式强制类型转换,然后再比较。
a == b
a != b
a === b
a !== b
TIP
在设计 JavaScript 1.0 时,为了满足用户请求,以简化与 HTTP/HTML 的集成,添加了一些有问题的转换规则。==
和 !=
操作符被设计成了可以在不同类型操作数之间通过隐式强制类型转换来比较。
在 ES1 发布之前,Brendan Eich 希望将 JavaScript 1.2 中修改了语义并消除强制类型转换问题的 ==
和 !=
操作符加入到 ES1 中。但被 Don't break the web 的理由说服了,最终并没有加入到 ES1 中。
为了弥补 ==
和 !=
操作符存在强制类型转换的不足,ES3 增加了不允许强制类型转换的 ===
和 !==
操作符。
# 逻辑表达式
逻辑表达式不仅可以针对布尔值进行布尔运算并返回布尔值,还可以用于假值或者空值(null 和 undefined)进行短路运算并返回两个操作数中的其中一个或者 undefined。
# 逻辑非
逻辑非(!
)操作符是一个一元操作符,用于将操作数进行逻辑非运算,也就是该操作符首先会将操作数转换为布尔值,然后在对其取反。如果操作数是布尔值则直接取反;如果操作数是假值则返回 true,如果操作数为真值则返回 false。
!a
# 逻辑与
逻辑与(&&
)操作符用于将操作数进行逻辑与运算。如果第一个操作数为真值,则返回第二个操作数,否则返回第一个操作数。
a && a.b
我们经常使用 &&
的短路运算机制为成员访问或调用表达式兜底,防止其在求值过程中报错,并且写出更简洁的代码。
# 逻辑或
逻辑或(||
)操作符用于将操作数进行逻辑或运算。如果第一个操作数为假值,则返回第二个操作数,否则返回第一个操作数。
a || b
同样,我们经常使用 ||
的短路运算机制为变量赋值提供默认值。
可以使用德·摩根定律简化逻辑表达式:
!a || !b === !(a && b)
!a && !b === !(a || b)
# 空值合并
ES2020 增加了空值合并(??
)操作符,用于给空值提供默认值。如果左侧的操作数是空值,则返回右侧操作数,否则返回左侧操作数。
a ?? b
等价于
a === null || a === undefined ? b : a
??
相当于简化版的 ||
,前者只有左侧操作数为空值才返回右侧操作数,后者只要左侧为假值就返回右侧操作数。??
在与 &&
或 ||
混合使用时存在优先级问题,需要使用括号明确指定谁先执行,否则会抛出语法错误。
a && b ?? c // SyntaxError
(a && b) ?? c
a && (b ?? c)
# 可选链
ES2020 增加了可选链(?.
)操作符,用于可选的访问对象成员或者调用函数。如果左侧操作数是空值,则返回 undefined,否则才会执行对象成员访问或者函数调用操作并返回相应的值。
a?.b
a?.[b]
a?.()
在 ES2020 之前,为了避免访问未定义的对象成员而导致的 TypeError
,我们通常使用以下方式:
a && a.b
而现在我们只需这样使用:
a?.b
还可以与 ??
搭配使用为空值提供默认值:
a?.b ?? v
值的注意的是,使用括号将会限制 ?.
短路的范围:
(a?.b).c
以上操作将会导致 TypeError
,而去掉括号则返回 undefined。
与对象成员访问和函数调用表达式的区别在于,可选链逻辑表达式不能用于赋值,这会导致 SyntaxError
。
# 条件表达式
条件表达式由 ?:
操作符构成,?:
有三个操作数。如果第一个操作数是假值,则求值第三个操作数并返回其值;否则求值第二个操作数并返回其值。
a ? b : c
# 赋值表达式
赋值表达式用于将右侧操作数的值赋值给左侧操作数并返回右侧操作数的值。
# 基本赋值
a = b
# 复合赋值
a += b // 等价于 a = a + b
a -= b // 等价于 a = a - b
a *= b // 等价于 a = a * b
a /= b // 等价于 a = a / b
a %= b // 等价于 a = a % b
a <<= b // 等价于 a = a << b
a >>= b // 等价于 a = a >> b
a >>>= b // 等价于 a = a >>> b
a &= b // 等价于 a = a & b
a |= b // 等价于 a = a | b
a ^= b // 等价于 a = a ^ b
// ES2017 新增
a **= b // 等价于 a = a ** b
# 逻辑赋值
ES2021 新增了 3 个逻辑赋值操作符,这些操作符与上述赋值操作符的区别在于可以利用短路机制避免产生赋值等其他副作用,而且还让代码变得更简洁:
a &&= b // 等价于 a && (a = b) 而不等价于 a = a && b
a ||= b // 等价于 a || (a = b) 而不等价于 a = a || b
a ??= b // 等价于 a ?? (a = b) 而不等价于 a = a ?? b
# 解构赋值
在 ES6 以前,想要批量提取特定数组的元素和对象的成员需要使用成员访问表达式通过点号或者方括号逐个提取。为了简化这种提取数据的过程,ES6 增加了解构赋值特性,解构赋值经常用于变量赋值和函数参数中。
const obj = { a: 1, b: 2, c: 3 };
// ES6 以前
obj.a;
obj[b];
// ES6
{a, b} = obj
右侧的操作数是一个数组或对象,左侧的操作数则是长得像数组或者对象字面量的赋值模式。
- 数组解构
数组解构允许我们使用长得像数组字面量的数组模式批量提取数组元素。
数组解构根据元素在数组中的位置进行提取元素,数组模式中的引用对应着数组字面量中的元素。
- 如果想要元素跳过前面某个元素,则不需要在数组模式中提供引用,而是使用逗号占位;
[a, ,c] = [2, 3, 5] // a => 2, c => 5
- 如果想要提取元素后面剩余的所有元素,则可以在数组模式中使用 ES6 提供的剩余元素(必须放在最后)语法;
[a, ...b] = [2, 3, 5] // a => 2, b => [3, 5]
- 如果提取到了缺失的元素,将会得到
undefined
,可以在数组模式中通过等号提供一个默认值。
[a, b = 0] = [2] // a => 2, b => 0
在 ES6 之前,交换变量值需要使用额外的临时变量,而在 ES6 中,使用数组解构语法可以简化交换变量值操作。
let a = 1;
let b = 2;
// ES6 之前
let temp = a;
a = b;
b = temp;
// ES6
[a, b] = [b, a] // a => 2, b => 1
数组解构不仅可以用于解构数组元素,还可以用于解构可迭代对象的值。
[a, b] = '12' // a => 1, b => 2
- 对象解构
对象解构允许我们使用长得像对象字面量的对象模式批量提取属性值。对象模式中的引用对应着对象字面量中的属性名。
({ a, b } = { a: 1, b: 2 }) // a => 1, b => 2
- 如果想重命名提取的属性,可以使用冒号提供别名。
({ a: c, b: d } = { a: 1, b: 2 }) // c => 1, d => 2
- 如果提取到缺失的属性,将会得到
undefined
,可以在对象模式中使用等号为其提供默认值。
({ a, b, c } = { a: 1, b: 2 }) // c => undefined
({ a, b, c = 3 } = { a: 1, b: 2 }) // c => 3
- 如果想要提取当前属性后面的所有属性,可以在对象模式中使用 ES2018 提供的剩余属性(必须放在最后)语法。
({ a, ...d } = { a: 1, b: 2, c: 3 }) // a => 1, d => { b: 2, c: 3 }
TIP
当对象解构前面没有变量声明关键字时,必须使用 ()
包括整个赋值表达式,不然左侧的的字面量赋值模式会被视为块,从而导致 SyntaxError
。
无论是数组解构还是对象解构,它们都支持嵌套解构,甚至混合解构,不过一定要适度使用,否则会让代码变得难以理解。
{ a: { b }, c: [d, e] } = { a: { b: 1 }, c: [2, 3] } // b => 1, d => 2, e => 3
# 其他操作符
# 逗号操作符
逗号(,
)操作符有两个操作数,首先先求值左侧操作数,然后求值右侧操作数,最后返回右侧操作数的值。
a = 1, b = 2 // => 2
# typeof 操作符
typeof
操作符用于检测右侧操作数类型返回表示操作数类型的字符串。
# delete 操作符
delete
操作符用于删除为左值的操作数,如果操作数不是左值,则什么都不做并返回 true;否则删除的是左值,如果删除成功则返回 true,否则返回 false。
# void 操作符
void
操作符用于求值右侧操作数并返回 undefined
。
# in 操作符
in
操作符用于判断左侧操作数是不是右侧操作数的属性。如果左侧操作数的值是右侧操作数的属性,则返回 true,否则返回 false。
# instanceof 操作符
instanceof
操作符用于判断左侧操作数是不是右侧操作数的实例。如果左侧操作数的值是右侧操作数的实例,则返回 true,否则返回 false。
# 语句
JavaScript 应用程序由一系列语句组成,而语句是由分号(;
)分隔的句子或命令。语句相当于自然语言中的句子。
# 简单语句
# 空语句
仅使用分号的语句,表示没有语句,常用在分支或者循环语句没有语句体的情况。
;
# 语句块
使用大括号({}
)组合零个或多个语句的语句块(复合语句),有意思的是,语句块在语法上被视为一个语句,常用在分支语句和循环语句的语句体,也可以单独作为语句块使用:
{
[StatementList]
}
注意,语句块结尾没有分号。
# 声明语句
声明语句使用关键字作为前缀,用于将值与标识符名称建立绑定,以方便其它实体(表达式、语句等)引用该值。ES6 以前只有一个使用 var 作为关键字前缀的变量声明语句,不过由于 var
语句的在运行时比较诡异,所以 ES6 新增了 let
和 const
语句弥补了前者的缺陷。
# var
语句
var name1 [= value1] [, name2 [= value2] [, ... nameN [= valueN]]];
let
语句用于声明变量。其特点如下:
- 初始化是可选的,如果没有初始化该变量的值将会是
undefined
; - 允许重复声明同名变量;
- 允许重新赋值;
- 在函数外声明的全局变量会成为全局对象的属性。
var userName; // => undefined
var userName = 'front-boy'; // window.userName === 'front-boy'
userName = 'old-boy';
# let
语句
let name1 [= value1] [, name2 [= value2] [, ... nameN [= valueN]]];
let
语句用于可变的变量绑定。其特点如下:
- 初始化也是可选的,如果没有初始化该变量的值将会是
undefined
; - 不允许重复声明同名变量,否则将会抛出
SyntaxError
; - 支持重新赋值;
- 在函数外声明的全局变量不会成为全局对象的属性。
let userName; // => undefined
let userName = 'front-boy'; // SyntaxError
userName = 'old-boy'; // window.userName === 'undefined'
# const
语句
const name1 = value1 [, name2 = value2 [, ... nameN = valueN]];
const
语句用于不可变的变量绑定。其特点如下:
- 在声明变量的同时必须初始化变量;
- 不允许重复声明同名变量,否则将会抛出
SyntaxError
; - 不允许重新赋值,否则将会抛出
TypeError
; - 在函数外声明的全局变量不会成为全局对象的属性。
const userName; // => SyntaxError
const userName = 'front-boy'; // window.userName === 'undefined'
userName = 'old-boy'; // TypeError
声明语句和解构赋值配合使用可以为提取值一次性声明多个变量。
const userInfo = { userName: 'front-boy', age: 24 };
let { userName, age } = userInfo;
# 表达式语句
表达式作为短语本身就是一种语句,只需在表达式后面加上分号即可。
需要注意的是,在使用无声明的对象结构赋值时,必须使用圆括号包围赋值语句,否则左侧的对象模式会被认为是语句块,从而抛出 SyntaxError
。
let a, b;
// { a, b } = { a: 1, b: 2 }; // SyntaxError
({ a, b } = { a: 1, b: 2 });
# 流程控制语句
流程控制语句简称流控语句,用于产生各种结构和控制语句执行。
# 分支语句
分支语句会产生分支结构,根据条件选择性的执行或跳过语句。
if
语句
if
语句只包含 if
和可选的 else
分支子句,并没有 else if
多分支子句。
if (condition)
statement1
[else
statement2]
如果条件表达式求值结果为真值,则执行 statement1,否则求值结果为假值,则执行 statement2。statement1 和 statement2 只能是一个语句,可以使用语句块将多条语句组合成一个语句。
if
和 else
子句中的语句可以嵌套 if
语句,所以即使 JavaScript 中没有 else if
子句,也可以通过在 else
子句中嵌套 if
语句形成多分支语句。
if (condition1) {
// ...
} else if (condition2) {
// ...
} else {
// ...
}
其本质其实就是:
if (condition1) {
// ...
} else {
if (condition2) {
// ...
} else {
// ...
}
}
switch
语句
switch
语句也可以用来表示多分支语句。switch
语句包含零个或多个 case 子句,还有一个可选的 default
子句。
switch (expression) {
case expression:
statementList
[break;]
[default:
statementList
[break;]]
}
switch
语句首先会对表达式求值,然后依次匹配与 case 子句表达式的值严格相等的子句。
- 如果匹配到则执行对应的语句列表,
- 如果
case
子句后面有break
语句,则会跳出switch
语句, - 否则继续执行其他
case
子句;
- 如果
- 如果没有匹配case 子句,则会去寻找可选的
default
子句,- 如果找到则执行
default
子句对应的语句列表, - 否则继续执行直到
switch
语句结束。
- 如果找到则执行
WARNING
需要注意的是,使用 switch
语句用于多分支语句时,千万不要忘记使用 break
语句,如果故意为之请加以注释!
# 循环语句
循环语句会产生循环结构,用于执行重复代码。循环语句都有一个称为循环体的语句,如果想在循环体中执行多条语句,可以使用语句块;如果不想执行任何语句,可以使用空语句。
while
语句
while
语句以 while
关键字开头,后面跟着一个使用括号包裹的条件表达式,最后是一个语句。
while (condition) statement
while
语句首先会对条件表达式求值:
- 如果为真值,则会执行循环体,然后继续对条件表达式求值;
- 如果为假值,则会跳出循环执行后面的语句;
- 如果始终为真值,该循环则是一个无限循环。
do-while
语句
do-while
语句以 do
关键字开头,后面跟着一个语句,然后使用 while
关键字,后面跟着一个用括号包裹的条件表达式,最后以分号结尾。
do statement while (condition);
do-while
循环的工作方式与 while
循环类似,只不过它是先执行循环体,然后再对条件表达式求值,也就是至少执行一次循环体。
for
语句
for
语句以 for
关键字开头,然后是被括号包括的初始化表达式、条件表达式和迭代后执行的表达式,它们都是可选的,最后是循环体。
for ([initialization]; [condition]; [final-expression]) statement
for
语句首先会在循环开始之前定义初始值,然后对条件表达式求值,如果为真值,则执行循环体,最后执行迭代后执行的表达式;如果为假值,则跳出循环执行后面的语句。如果 for
语句的三个表达式都被省略了,则会创建一个无限循环。
for (;;) {
// ...
}
对于 while
循环,for
循环只是将循环相关的代码封装在了一起而已。而 while
循环则是这样构建代码:
initialization;
while (condition) {
statementList
final-expression;
}
for-in
语句
for-in
语句用于迭代对象自身或者继承的可枚举属性,迭代的顺序无序的。
for (variable in object) statement
for-in
语句首先对 object 表达式求值,如果为空值则跳出循环执行后面的语句。否则对 variable 表达式求值,并将属性名赋值给它。最后执行循环体。
WARNING
for-in
循环不适合用于迭代数组。
for-of
语句
为了支持(同步)迭代可迭代对象,ES6 增加了 for-of
循环。
for (variable of iterable) statement
for-await-of
语句
为了支持迭代异步可迭代对象,ES2018 增加了 for-await-of
循环,它只适用于异步函数。
for await (variable of iterable) statement
# 跳转语句
跳转语句会让语句跳转到特定位置,不过跳转语句受限于上下文环境。
- 标签语句
标签语句用于给语句命名,以便 break
和 continue
语句引用。
label: statement
label 可以是任意合法的标识符。标签语句通常应用于嵌套循环。
break
语句
break
语句用于提前退出当前循环、switch
语句和标签语句,使语句跳转至下一个语句执行。
break [label];
outer: for (let i = 0; i < 2; i++) {
inner: for (let j = 0; j < 2; j++) {
if (i === 1 && j === 1) break outer;
}
}
break
语句只能嵌套在上述三种语句中,否则会导致语法错误。
continue
语句
continue
语句用于提前退出当前循环并跳转至循环体顶部执行下一轮循环。continue
语句也可以与带有 label 的循环体配合使用,用于提前退出当前循环并跳转至 label 命名的循环继续执行。
continue [label];
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (i === j) continue;
console.log(i, j);
}
}
continue
语句只能应用于循环体内部,否则会导致语法错误。
return
语句
return
语句用于终止函数执行并返回表达式的值,如果省略了表达式则返回 undefined
,最终带着值跳转回函数调用方。
return [expression];
return
语句只能应用于函数内部,否则会导致语法错误。
# 异常处理语句
程序可能存在一些未知的 bug,异常处理语句可以为我们查找、诊断和修复这些 bug。
throw
语句
throw
语句用于抛出自定义异常,表示程序发生了某种意外或者错误。expression 可以是任意类型的值,不过因为 Error
构造函数提供了沿着函数调用栈传递的异常信息(异常发生的位置和导致失败的函数),所以通常使用 Error
构造函数作为异常值。
throw expression;
throw
语句存在一个机制,就是抛出异常会让当前程序停止执行并跳转到最近的异常处理程序继续执行。异常会通过函数调用栈层层传播,直到找到异常处理程序处理该异常,否则异常将会抛出一个未捕获的错误。
try
语句
try
语句是 JavaScript 的异常处理程序,用于捕获、处理和退出异常。
try {
statementList
}
[catch[(exception)] {
statementList
}]
[finally {
statementList
}]
try
语句包含 try
子句、catch
子句和 finally
子句三个部分。
try
子句用于捕获异常catch
子句(可选)用于处理异常
当 try
语句捕获到程序执行的错误或者使用 throw
语句抛出的错误,程序将会跳转到 catch
子句并执行异常处理相关的代码。其中,接收一个表示异常对象 exception 参数,ES2019 支持省略该参数。
finally
子句(可选)用于清理异常代码并退出异常
无论 try
子句或者 catch
子句发生了什么,只要包含 finally
子句一定会在上述两种语句执行后执行。
虽然 catch
子句和 finally
子句是可选的,不过一定要包含其中一个子句。
debugger
语句
debugger
语句用于在开发阶段为程序提供类似于断点的功能,方便在程序出现异常或者错误时调试。
# 自动插入分号机制
自动插入分号机制(Automatic Semicolon Insertion,简称 ASI),是一种推断某些上下文中省略的分号,然后有效地自动将分号插入到程序中的程序解析技术。ASI 让分号作为语句终结符成为了一种可选方案。
ASI 在解析时有一些陷阱,在语法上也有额外的限制,我们只需了解以下规则,就可以享受 ASI 给我们带来的便利。
- 分号仅在语句块
}
之前、一行和程序的结尾插入;
// 解析前
let a = 1, b = 2
function add(a, b) { return a + b }
add(a, b)
// 解析后
let a = 1, b = 2;
function add(a, b) { return a + b; }
add(a, b);
- ASI 是一种错误校正的机制,分号仅在输入的标记不能解析时插入;
let a = 1
console.log(a)
由于第一行末尾和第二行开头无法构成合法的标记,程序无法解析这种非法的标记,所以会在第一行末尾插入分号。
不过,下一条语句开始和上一条语句的结束标记能够合法解析,则不会插入分号,主要包括以下 5 种情况:
- 以
(
开头被视为调用表达式;
// 解析前
(function() {
// ...
})()
(function() {
// ...
})()
// 解析后
(function() {
// ...
})()(function() {
// ...
})();
- 以
[
开头被视为属性访问表达式;
// 解析前
a = b
[1, 2].forEach(c => console.log(c))
// 解析后
a = b[1, 2].forEach(c => console.log(c));
- 以
`
开头被视为标签模板
// 解析前
a = b
`bar`.match('b')
// 解析后
a = b`bar`.match('b');
- 以正则表达式字面量开头被视为除法运算符;
// 解析前
a = b
/bar/i.test('b')
// 解析后
a = b / bar /i.test('b');
- 以
+
和-
开头被视为加或减运算符。
// 解析前
a = b
+ c
- d
// 解析后
a = b + c - d;
如果出现以上几种情况,需要在前面插入分号防御。
- 语法限制产生式的地方不能插入换行,即便不会出现解析错误,仍会强制插入分号。
return
关键字与表达式之间不能出现换行;
// 解析前
return
a + b
// 解析后
return;
a + b;
throw
关键字与表达式之间不能插入换行;break
或continue
关键字与标签之间不能插入换行;后缀自增自减表达式操作数与操作符之间不能插入换行;
// 解析前
a
++
b
// 解析后
a;
++ b;
yield
关键字与表达式之间不能插入换行;箭头函数参数与胖箭头(
=>
)之间不能插入换行;async
关键字与方法或函数之间不能插入换行。