词法语法

本页介绍 JavaScript 的词法语法。JavaScript 源代码只是一系列字符——为了让解释器理解它,字符串必须被**解析**成更结构化的表示。解析的初始步骤称为词法分析,在此步骤中,文本从左到右扫描并转换为一系列独立的、原子的输入元素。某些输入元素对解释器来说不重要,在此步骤后将被删除——它们包括空白字符注释。其他元素,包括标识符关键字字面量和标点符号(主要是运算符),将用于进一步的语法分析。行终止符和多行注释在语法上也不重要,但它们指导自动分号插入过程,使某些无效的标记序列变为有效。

格式控制字符

格式控制字符没有视觉表示,但用于控制文本的解释。

码点 名称 缩写 描述
U+200C 零宽度非连接符 <ZWNJ> 放置在字符之间,以防止在某些语言中连接成连字 (维基百科)。
U+200D 零宽度连接符 <ZWJ> 放置在通常不会连接的字符之间,以使字符在某些语言中以连接形式呈现 (维基百科)。
U+FEFF 字节顺序标记 <BOM> 用于脚本开头,以将其标记为 Unicode 并允许检测文本的编码和字节顺序 (维基百科)。

在 JavaScript 源代码中,<ZWNJ> 和 <ZWJ> 被视为标识符部分,而 <BOM>(当不在文本开头时也称为零宽度不间断空格 <ZWNBSP>)被视为空白字符

空白字符

空白字符提高了源代码的可读性并分隔了标记。这些字符通常对于代码的功能来说是不必要的。压缩工具通常用于删除空白以减少需要传输的数据量。

码点 名称 缩写 描述 转义序列
U+0009 字符制表符 <TAB> 水平制表符 \t
U+000B 行制表符 <VT> 垂直制表符 \v
U+000C 换页 <FF> 分页控制字符 (维基百科)。 \f
U+0020 空格 <SP> 普通空格
U+00A0 不间断空格 <NBSP> 普通空格,但不能在此处断行
U+FEFF 零宽度不间断空格 <ZWNBSP> 当不在脚本开头时,BOM 标记是普通的空白字符。
其他 其他 Unicode 空格字符 <USP> "Space_Separator" 通用类别中的字符

注意:具有 "White_Space" 属性但不在 "Space_Separator" 通用类别中的字符中,U+0009、U+000B 和 U+000C 仍被视为 JavaScript 中的空白字符;U+0085 NEXT LINE 没有特殊作用;其他字符成为行终止符集。

注意:JavaScript 引擎使用的 Unicode 标准的更改可能会影响程序的行为。例如,ES2016 将参考 Unicode 标准从 5.1 升级到 8.0.0,这导致 U+180E MONGOLIAN VOWEL SEPARATOR 从 "Space_Separator" 类别移动到 "Format (Cf)" 类别,并使其成为非空白字符。随后,"\u180E".trim().length 的结果从 0 变为 1

行终止符

除了空白字符,行终止符字符也用于提高源代码的可读性。然而,在某些情况下,行终止符会影响 JavaScript 代码的执行,因为在某些地方它们是被禁止的。行终止符还会影响自动分号插入的过程。

在词法语法的上下文之外,空白和行终止符通常被混淆。例如,String.prototype.trim() 从字符串的开头和结尾删除所有空白和行终止符。正则表达式中的 \s 字符类转义匹配所有空白和行终止符。

只有以下 Unicode 码点在 ECMAScript 中被视为行终止符,其他换行符被视为空白字符(例如,下一行,NEL,U+0085 被视为空白字符)。

码点 名称 缩写 描述 转义序列
U+000A 换行符 <LF> UNIX 系统中的换行符。 \n
U+000D 回车符 <CR> Commodore 和早期 Mac 系统中的换行符。 \r
U+2028 行分隔符 <LS> 维基百科
U+2029 段落分隔符 <PS> 维基百科

注释

注释用于向 JavaScript 代码添加提示、备注、建议或警告。这可以使其更容易阅读和理解。它们还可以用于禁用代码以防止其执行;这可能是一个有价值的调试工具。

JavaScript 有两种长期存在的方式向代码添加注释:行注释和块注释。此外,还有一种特殊的哈希标记注释语法。

行注释

第一种方式是 // 注释;这使得同一行中其后的所有文本成为注释。例如:

js
function comment() {
  // This is a one line JavaScript comment
  console.log("Hello world!");
}
comment();

块注释

第二种方式是 /* */ 样式,这种方式更加灵活。

例如,你可以在单行中使用它

js
function comment() {
  /* This is a one line JavaScript comment */
  console.log("Hello world!");
}
comment();

你也可以创建多行注释,像这样

js
function comment() {
  /* This comment spans multiple lines. Notice
     that we don't need to end the comment until we're done. */
  console.log("Hello world!");
}
comment();

如果你愿意,你也可以在行中间使用它,尽管这可能会使你的代码难以阅读,所以应该谨慎使用

js
function comment(x) {
  console.log("Hello " + x /* insert the value of x */ + " !");
}
comment("world");

此外,你可以通过将代码包装在注释中来禁用代码以防止其运行,像这样

js
function comment() {
  /* console.log("Hello world!"); */
}
comment();

在这种情况下,console.log() 调用永远不会被发出,因为它在注释内部。任何数量的代码行都可以通过这种方式被禁用。

包含至少一个行终止符的块注释在自动分号插入中表现得像行终止符

Hashbang 注释

还有一种特殊的第三种注释语法,即**hashbang 注释**。hashbang 注释的行为与单行(//)注释完全相同,不同之处在于它以 #! 开头,并且**只在脚本或模块的绝对开头有效**。另请注意,在 #! 之前不允许有任何类型的空白。注释由 #! 之后直到第一行末尾的所有字符组成;只允许有一个这样的注释。

JavaScript 中的 Hashbang 注释类似于Unix 中的 shebang,它提供了你想要用于执行脚本的特定 JavaScript 解释器的路径。在 hashbang 注释标准化之前,它已经在 Node.js 等非浏览器宿主中被事实实现,在那里它在传递给引擎之前从源代码文本中删除。示例如下:

js
#!/usr/bin/env node

console.log("Hello world");

JavaScript 解释器会将其视为普通注释——只有当脚本在 shell 中直接运行时,它才对 shell 具有语义意义。

警告:如果你想让脚本在 shell 环境中直接运行,请以 UTF-8 编码,不带BOM。尽管 BOM 不会导致在浏览器中运行的代码出现任何问题——因为它在 UTF-8 解码期间被删除,在源代码文本被分析之前——但如果 hashbang 前面有一个 BOM 字符,Unix/Linux shell 将无法识别它。

你必须只使用 #! 注释样式来指定 JavaScript 解释器。在所有其他情况下,只需使用 // 注释(或多行注释)。

标识符

标识符用于将值与名称关联起来。标识符可以在各种地方使用

js
const decl = 1; // Variable declaration (may also be `let` or `var`)
function fn() {} // Function declaration
const obj = { key: "value" }; // Object keys
// Class declaration
class C {
  #priv = "value"; // Private field
}
lbl: console.log(1); // Label

在 JavaScript 中,标识符通常由字母数字字符、下划线 (_) 和美元符号 ($) 组成。标识符不能以数字开头。然而,JavaScript 标识符不仅限于 ASCII — 许多 Unicode 码点也允许使用。具体来说:

  • 起始字符可以是ID_Start 类别中的任何字符,加上 _$
  • 在第一个字符之后,你可以使用ID_Continue 类别中的任何字符,加上 U+200C (ZWNJ) 和 U+200D (ZWJ)。

注意:如果出于某种原因,你需要自己解析某些 JavaScript 源代码,请不要假设所有标识符都遵循模式 /[A-Za-z_$][\w$]*/(即,仅 ASCII)!标识符的范围可以用正则表达式 /[$_\p{ID_Start}][$\p{ID_Continue}]*/u 来描述(不包括 Unicode 转义序列)。

此外,JavaScript 允许在标识符中使用 \u0000\u{000000} 形式的Unicode 转义序列,它们编码的字符串值与实际的 Unicode 字符相同。例如,你好\u4f60\u597d 是相同的标识符

js
const 你好 = "Hello";
console.log(\u4f60\u597d); // Hello

并非所有地方都接受完整的标识符范围。某些语法,例如函数声明、函数表达式和变量声明,要求使用不是保留字的标识符名称。

js
function import() {} // Illegal: import is a reserved word.

最值得注意的是,私有元素和对象属性允许使用保留字。

js
const obj = { import: "value" }; // Legal despite `import` being reserved
class C {
  #import = "value";
}

关键词

关键字是看起来像标识符但在 JavaScript 中具有特殊含义的标记。例如,函数声明前的关键字 async 表示该函数是异步的。

有些关键字是**保留**的,这意味着它们不能用作变量声明、函数声明等的标识符。它们通常被称为**保留字**。保留字的列表如下所示。并非所有关键字都是保留的——例如,async 可以在任何地方用作标识符。有些关键字只在**上下文中保留**——例如,await 只在异步函数体中保留,let 只在严格模式代码中或 constlet 声明中保留。

标识符总是按*字符串值*比较,因此转义序列会被解释。例如,这仍然是一个语法错误

js
const els\u{65} = 1;
// `els\u{65}` encodes the same identifier as `else`

保留字

这些关键字不能在 JavaScript 源的任何地方用作变量、函数、类等的标识符。

以下仅在严格模式代码中被保留

  • let (在 const, let 和类声明中也保留)
  • static
  • yield (在生成器函数体中也保留)

以下仅在模块代码或异步函数体中被保留

未来保留字

以下是 ECMAScript 规范保留的未来关键字。它们目前没有特殊功能,但将来可能会有,因此不能用作标识符。

这些始终保留

  • enum

以下仅在严格模式代码中被保留

  • implements
  • interface
  • package
  • private
  • protected
  • public

旧标准中的未来保留字

以下是旧版 ECMAScript 规范(ECMAScript 1 到 3)保留的未来关键字。

  • abstract
  • boolean
  • byte
  • char
  • double
  • final
  • float
  • goto
  • int
  • long
  • native
  • short
  • synchronized
  • throws
  • transient
  • volatile

具有特殊含义的标识符

少数标识符在某些上下文中具有特殊含义,但它们并非任何类型的保留字。它们包括

字面量

注意:本节讨论作为原子标记的字面量。对象字面量数组字面量是包含一系列标记的表达式

Null 字面量

另请参阅 null 以获取更多信息。

js
null

布尔字面量

另请参阅布尔类型以获取更多信息。

js
true
false

数值字面量

NumberBigInt 类型使用数值字面量。

十进制

js
1234567890
42

十进制字面量可以以零 (0) 开头,后跟另一个十进制数字,但如果前导 0 之后的所有数字都小于 8,则该数字被解释为八进制数。这被视为一种遗留语法,以 0 为前缀的数字字面量,无论被解释为八进制还是十进制,在严格模式下都会导致语法错误——因此,请改用 0o 前缀。

js
0888 // 888 parsed as decimal
0777 // parsed as octal, 511 in decimal
指数

十进制指数字面量由以下格式指定:beN;其中 b 是基数(整数或浮点数),后跟 Ee 字符(用作分隔符或*指数指示符*)和 N,它是一个带符号整数的*指数*或*幂*数。

js
0e-5   // 0
0e+5   // 0
5e1    // 50
175e-2 // 1.75
1e3    // 1000
1e-3   // 0.001
1E3    // 1000

二进制

二进制数字语法使用前导零后跟小写或大写拉丁字母 "B" (0b0B)。0b 后任何不是 0 或 1 的字符都将终止字面量序列。

js
0b10000000000000000000000000000000 // 2147483648
0b01111111100000000000000000000000 // 2139095040
0B00000000011111111111111111111111 // 8388607

八进制

八进制数字语法使用前导零后跟小写或大写拉丁字母 "O" (0o0O)。0o 后任何超出范围 (01234567) 的字符都将终止字面量序列。

js
0O755 // 493
0o644 // 420

十六进制

十六进制数字语法使用前导零后跟小写或大写拉丁字母 "X" (0x0X)。0x 后任何超出范围 (0123456789ABCDEF) 的字符都将终止字面量序列。

js
0xFFFFFFFFFFFFF // 4503599627370495
0xabcdef123456  // 188900967593046
0XA             // 10

BigInt 字面量

BigInt 类型是 JavaScript 中的一种数值基本类型,可以表示任意精度的整数。BigInt 字面量通过在整数末尾附加 n 创建。

js
123456789123456789n     // 123456789123456789
0o777777777777n         // 68719476735
0x123456789ABCDEFn      // 81985529216486895
0b11101001010101010101n // 955733

BigInt 字面量不能以 0 开头,以避免与遗留的八进制字面量混淆。

js
0755n; // SyntaxError: invalid BigInt syntax

对于八进制 BigInt 数字,始终使用零后跟字母 "o"(大写或小写)

js
0o755n;

有关 BigInt 的更多信息,另请参阅JavaScript 数据结构

数字分隔符

为了提高数值字面量的可读性,可以使用下划线 (_, U+005F) 作为分隔符

js
1_000_000_000_000
1_050.95
0b1010_0001_1000_0101
0o2_2_5_6
0xA0_B0_C0
1_000_000_000_000_000_000_000n

请注意这些限制

js
// More than one underscore in a row is not allowed
100__000; // SyntaxError

// Not allowed at the end of numeric literals
100_; // SyntaxError

// Can not be used after leading 0
0_1; // SyntaxError

字符串字面量

一个字符串字面量是由单引号或双引号括起来的零个或多个 Unicode 码点。Unicode 码点也可以由转义序列表示。除了这些码点之外,所有码点都可以字面量形式出现在字符串字面量中

  • U+005C \ (反斜杠)
  • U+000D <CR>
  • U+000A <LF>
  • 与字符串字面量开头相同的引号

任何码点都可以以转义序列的形式出现。字符串字面量求值为 ECMAScript 字符串值。生成这些字符串值时,Unicode 码点采用 UTF-16 编码。

js
'foo'
"bar"

以下小节描述了字符串字面量中可用的各种转义序列(\ 后跟一个或多个字符)。未列出的任何转义序列都将成为“身份转义”,它将成为码点本身。例如,\zz 相同。在已弃用和过时功能页面中描述了已弃用的八进制转义序列语法。其中许多转义序列在正则表达式中也有效——参见字符转义

转义序列

特殊字符可以使用转义序列编码

转义序列 Unicode 码点
\0 空字符 (U+0000 NULL)
\' 单引号 (U+0027 APOSTROPHE)
\" 双引号 (U+0022 QUOTATION MARK)
\\ 反斜杠 (U+005C REVERSE SOLIDUS)
\n 换行 (U+000A LINE FEED; LF)
\r 回车 (U+000D CARRIAGE RETURN; CR)
\v 垂直制表符 (U+000B LINE TABULATION)
\t 制表符 (U+0009 CHARACTER TABULATION)
\b 退格 (U+0008 BACKSPACE)
\f 换页 (U+000C FORM FEED)
\ 后跟行终止符 空字符串

最后一个转义序列,\ 后跟一个行终止符,对于跨多行拆分字符串字面量而又不改变其含义非常有用。

js
const longString =
  "This is a very long string which needs \
to wrap across multiple lines because \
otherwise my code is unreadable.";

确保反斜杠后面没有空格或任何其他字符(除了换行符),否则它将不起作用。如果下一行缩进,额外的空格也将存在于字符串的值中。

你也可以使用+ 运算符来连接多个字符串,像这样

js
const longString =
  "This is a very long string which needs " +
  "to wrap across multiple lines because " +
  "otherwise my code is unreadable.";

以上两种方法都产生相同的字符串。

十六进制转义序列

十六进制转义序列由 \x 后跟正好两位十六进制数字组成,表示范围为 0x0000 到 0x00FF 的代码单元或码点。

js
"\xA9"; // "©"

Unicode 转义序列

Unicode 转义序列由 \u 后跟正好四位十六进制数字组成。它表示 UTF-16 编码中的一个代码单元。对于 U+0000 到 U+FFFF 的码点,代码单元等于码点。U+10000 到 U+10FFFF 的码点需要两个转义序列来表示用于编码字符的两个代码单元(代理对);代理对与码点不同。

另请参阅 String.fromCharCode()String.prototype.charCodeAt()

js
"\u00A9"; // "©" (U+A9)

Unicode 码点转义

Unicode 码点转义由 \u{、后跟十六进制码点、再后跟 } 组成。十六进制数字的值必须在 0 到 0x10FFFF(含)之间。码点在 U+10000 到 U+10FFFF 范围内的无需表示为代理对。

另请参阅 String.fromCodePoint()String.prototype.codePointAt()

js
"\u{2F804}"; // CJK COMPATIBILITY IDEOGRAPH-2F804 (U+2F804)

// the same character represented as a surrogate pair
"\uD87E\uDC04";

正则表达式字面量

正则表达式字面量由两个正斜杠 (/) 括起来。词法分析器会消耗所有字符,直到下一个未转义的正斜杠或行尾,除非正斜杠出现在字符类 ([]) 内。某些字符(即那些是标识符部分的字符)可以出现在闭合斜杠之后,表示标志。

词法语法非常宽松:并非所有被识别为一个标记的正则表达式字面量都是有效的正则表达式。

另请参阅 RegExp 以获取更多信息。

js
/ab+c/g;
/[/]/;

正则表达式字面量不能以两个正斜杠 (//) 开头,因为那将是行注释。要指定空正则表达式,请使用 /(?:)/

模板字面量

一个模板字面量由几个标记组成:`xxx${(模板头)、}xxx${(模板中)和 }xxx`(模板尾)是独立的标记,而任何表达式都可以出现在它们之间。

另请参阅模板字面量以获取更多信息。

js
`string text`;

`string text line 1
 string text line 2`;

`string text ${expression} string text`;

tag`string text ${expression} string text`;

自动分号插入

一些 JavaScript 语句的语法定义要求在末尾使用分号 (;)。它们包括

然而,为了使语言更易于使用和方便,JavaScript 能够在消耗标记流时自动插入分号,以便一些无效的标记序列可以被“修复”为有效语法。此步骤在根据词法语法将程序文本解析为标记之后发生。在以下三种情况下会自动插入分号

1. 当遇到语法不允许的标记,并且它与前一个标记之间至少有一个行终止符(包括包含至少一个行终止符的块注释),或者该标记是“}”,则在该标记之前插入一个分号。

js
{ 1
2 } 3

// is transformed by ASI into:

{ 1
;2 ;} 3;

// Which is valid grammar encoding three statements,
// each consisting of a number literal

do...while 的结尾 ")" 也被此规则作为特例处理。

js
do {
  // …
} while (condition) /* ; */ // ASI here
const a = 1

然而,如果分号会成为 for 语句头部的分隔符,则不会插入分号。

js
for (
  let a = 1 // No ASI here
  a < 10 // No ASI here
  a++
) {}

分号也从不作为空语句插入。例如,在下面的代码中,如果分号在 ")" 之后插入,那么代码将是有效的,其中 if 主体是一个空语句,而 const 声明是一个单独的语句。然而,由于自动插入的分号不能成为空语句,这导致声明成为 if 语句的主体,这是无效的。

js
if (Math.random() > 0.5)
const x = 1 // SyntaxError: Unexpected token 'const'

2. 当到达输入标记流的末尾,并且解析器无法将单个输入流解析为完整的程序时,在末尾插入一个分号。

js
const a = 1 /* ; */ // ASI here

此规则是对前一个规则的补充,专门用于没有“违规标记”但输入流结束的情况。

3. 当语法禁止在某些位置出现行终止符但发现行终止符时,会插入分号。这些位置包括

  • expr <here> ++, expr <here> --
  • continue <here> lbl
  • break <here> lbl
  • return <here> expr
  • throw <here> expr
  • yield <here> expr
  • yield <here> * expr
  • (param) <here> => {}
  • async <here> function, async <here> prop(), async <here> function*, async <here> *prop(), async <here> (param) <here> => {}
  • using <here> id, await <here> using <here> id

这里 ++ 不被视为应用于变量 b 的后缀运算符,因为 b++ 之间出现了行终止符。

js
a = b
++c

// is transformed by ASI into

a = b;
++c;

这里,return 语句返回 undefined,而 a + b 成为不可达语句。

js
return
a + b

// is transformed by ASI into

return;
a + b;

请注意,ASI 仅在换行符分隔原本会产生无效语法的标记时才会被触发。如果下一个标记可以被解析为有效结构的一部分,则不会插入分号。例如

js
const a = 1
(1).toString()

const b = 1
[1, 2, 3].forEach(console.log)

由于 () 可以看作是函数调用,它通常不会触发 ASI。同样,[] 可能是成员访问。上面的代码等效于

js
const a = 1(1).toString();

const b = 1[1, 2, 3].forEach(console.log);

这恰好是有效的语法。1[1, 2, 3] 是一个带有逗号连接表达式的属性访问器。因此,在运行代码时会遇到“1 is not a function”和“Cannot read properties of undefined (reading 'forEach')”之类的错误。

在类中,类字段和生成器方法也可能是一个陷阱。

js
class A {
  a = 1
  *gen() {}
}

它被视为

js
class A {
  a = 1 * gen() {}
}

因此,在 { 附近会出现语法错误。

如果你想强制执行无分号风格,以下是一些处理 ASI 的经验法则

  • 将后缀 ++-- 写在其操作数同一行。

    js
    const a = b
    ++
    console.log(a) // ReferenceError: Invalid left-hand side expression in prefix operation
    
    js
    const a = b++
    console.log(a)
    
  • returnthrowyield 之后的表达式应与关键字在同一行。

    js
    function foo() {
      return
        1 + 1 // Returns undefined; 1 + 1 is ignored
    }
    
    js
    function foo() {
      return 1 + 1
    }
    
    function foo() {
      return (
        1 + 1
      )
    }
    
  • 同样,breakcontinue 之后的标签标识符应与关键字在同一行。

    js
    outerBlock: {
      innerBlock: {
        break
          outerBlock // SyntaxError: Illegal break statement
      }
    }
    
    js
    outerBlock: {
      innerBlock: {
        break outerBlock
      }
    }
    
  • 箭头函数的 => 应与其参数的末尾在同一行。

    js
    const foo = (a, b)
      => a + b
    
    js
    const foo = (a, b) =>
      a + b
    
  • 异步函数、方法等的 async 不能直接跟在行终止符之后。

    js
    async
    function foo() {}
    
    js
    async function
    foo() {}
    
  • usingawait using 语句中的 using 关键字应与其声明的第一个标识符在同一行。

    js
    using
    resource = acquireResource()
    
    js
    using resource
      = acquireResource()
    
  • 如果一行以 ([`+-/(如正则表达式字面量)之一开头,则在其前面加上分号,或在前一行末尾加上分号。

    js
    // The () may be merged with the previous line as a function call
    (() => {
      // …
    })()
    
    // The [ may be merged with the previous line as a property access
    [1, 2, 3].forEach(console.log)
    
    // The ` may be merged with the previous line as a tagged template literal
    `string text ${data}`.match(pattern).forEach(console.log)
    
    // The + may be merged with the previous line as a binary + expression
    +a.toString()
    
    // The - may be merged with the previous line as a binary - expression
    -a.toString()
    
    // The / may be merged with the previous line as a division expression
    /pattern/.exec(str).forEach(console.log)
    
    js
    ;(() => {
      // …
    })()
    ;[1, 2, 3].forEach(console.log)
    ;`string text ${data}`.match(pattern).forEach(console.log)
    ;+a.toString()
    ;-a.toString()
    ;/pattern/.exec(str).forEach(console.log)
    
  • 类字段最好始终以分号结尾——除了上一条规则(其中包括后跟计算属性的字段声明,因为后者以 [ 开头),字段声明和生成器方法之间也需要分号。

    js
    class A {
      a = 1
      [b] = 2
      *gen() {} // Seen as a = 1[b] = 2 * gen() {}
    }
    
    js
    class A {
      a = 1;
      [b] = 2;
      *gen() {}
    }
    

规范

规范
ECMAScript® 2026 语言规范

浏览器兼容性

另见