语法和类型

本章讨论 JavaScript 的基本语法、变量声明、数据类型和字面量。

基础知识

JavaScript 的大部分语法借鉴了 Java、C 和 C++,但也受到了 Awk、Perl 和 Python 的影响。

JavaScript 区分大小写,并使用 Unicode 字符集。例如,单词 Früh(德语中意为“早”)可以用作变量名。

js
const Früh = "foobar";

但是,变量 frühFrüh 不同,因为 JavaScript 区分大小写。

在 JavaScript 中,指令称为语句,并用分号 (;) 分隔。

如果语句单独成行,则不需要分号。但是,如果一行中有多个语句,则它们必须用分号分隔。

注意:ECMAScript 也有一套自动插入分号 (ASI) 来结束语句的规则。(更多信息,请参阅 JavaScript 词法语法的详细参考。)

然而,最佳实践是始终在语句后写入分号,即使在不严格需要的情况下也是如此。这种做法可以减少代码中出现错误的机会。

JavaScript 脚本的源代码从左到右进行扫描,并转换为一系列输入元素,这些元素是标记控制字符行终止符注释空白字符。(空格、制表符和换行符都被视为空白字符。)

注释

注释的语法与 C++ 和许多其他语言相同

js
// a one line comment

/* this is a longer,
 * multi-line comment
 */

您不能嵌套块注释。当您不小心在注释中包含了 */ 序列时,通常会发生这种情况,这将终止注释。

js
/* You can't, however, /* nest comments */ SyntaxError */

在这种情况下,您需要将 */ 模式拆开。例如,通过插入反斜杠

js
/* You can /* nest comments *\/ by escaping slashes */

注释行为类似于空白字符,并在脚本执行期间被忽略。

注意:您可能还会在某些 JavaScript 文件的开头看到第三种注释语法,它看起来像这样:#!/usr/bin/env node

这被称为 hashbang 注释语法,是一种特殊注释,用于指定应执行脚本的特定 JavaScript 引擎的路径。有关更多详细信息,请参阅Hashbang 注释

声明

JavaScript 有三种变量声明。

var

声明一个变量,并可选择将其初始化为一个值。

let

声明一个块作用域的局部变量,并可选择将其初始化为一个值。

const

声明一个块作用域的只读命名常量。

变量

您在应用程序中使用变量作为值的符号名称。变量的名称,称为标识符,遵循某些规则。

JavaScript 标识符通常以字母、下划线 (_) 或美元符号 ($) 开头。后续字符也可以是数字 (09)。由于 JavaScript 区分大小写,字母包括字符 AZ(大写)以及 az(小写)。

您可以在标识符中使用大多数 Unicode 字母,例如 åü。(有关更多详细信息,请参阅词法语法参考。)您还可以使用Unicode 转义序列来表示标识符中的字符。

一些合法的名称示例是 Number_hitstemp99$credit_name

声明变量

您可以通过两种方式声明变量

  • 使用关键字 var。例如,var x = 42。此语法可用于声明局部变量和全局变量,具体取决于执行上下文
  • 使用关键字 constlet。例如,let y = 13。此语法可用于声明块作用域的局部变量。(请参阅下面的变量作用域。)

您可以使用解构语法声明变量来解包值。例如,const { bar } = foo。这将创建一个名为 bar 的变量,并为其赋值为我们对象 foo 中同名键对应的值。

变量在使用前应始终声明。JavaScript 过去允许赋值给未声明的变量,这会创建一个未声明的全局变量。这在严格模式下是一个错误,应完全避免。

声明与初始化

在像 let x = 42 这样的语句中,let x 部分称为声明= 42 部分称为初始化器。声明允许变量在代码中稍后访问而不会抛出 ReferenceError,而初始化器为变量赋值。在 varlet 声明中,初始化器是可选的。如果变量未带初始化器声明,则它被赋值为 undefined

js
let x;
console.log(x); // logs "undefined"

本质上,let x = 42 等同于 let x; x = 42

const 声明总是需要一个初始化器,因为它们禁止在声明后进行任何类型的赋值,并且隐式地用 undefined 初始化很可能是程序员的错误。

js
const x; // SyntaxError: Missing initializer in const declaration

变量作用域

变量可能属于以下作用域之一

  • 全局作用域:脚本模式下所有代码的默认作用域。
  • 模块作用域:模块模式下代码的作用域。
  • 函数作用域:通过函数创建的作用域。

此外,用 letconst 声明的变量可以属于一个额外的作用域

  • 块作用域:由一对花括号(一个)创建的作用域。

当您在任何函数之外声明变量时,它被称为全局变量,因为它可用于当前文档中的任何其他代码。当您在函数内部声明变量时,它被称为局部变量,因为它仅在该函数内部可用。

letconst 声明也可以作用域到它们声明的块语句

js
if (Math.random() > 0.5) {
  const y = 5;
}
console.log(y); // ReferenceError: y is not defined

然而,用 var 创建的变量不是块作用域的,而只是块所在函数(或全局作用域)的局部变量。

例如,以下代码将输出 5,因为 x 的作用域是全局上下文(如果代码是函数的一部分,则为函数上下文)。x 的作用域不限于紧邻的 if 语句块。

js
if (true) {
  var x = 5;
}
console.log(x); // x is 5

变量提升

var 声明的变量会被提升,这意味着您可以在其作用域中的任何位置引用该变量,即使其声明尚未到达。您可以将 var 声明视为被“提升”到其函数或全局作用域的顶部。但是,如果您在变量声明之前访问它,其值始终为 undefined,因为只有其声明默认初始化(使用 undefined会被提升,而不是其值赋值

js
console.log(x === undefined); // true
var x = 3;

(function () {
  console.log(x); // undefined
  var x = "local value";
})();

上面的例子将被解释为与

js
var x;
console.log(x === undefined); // true
x = 3;

(function () {
  var x;
  console.log(x); // undefined
  x = "local value";
})();

由于提升,函数中的所有 var 语句都应尽可能地放置在函数的顶部。这种最佳实践提高了代码的清晰度。

letconst 是否提升是定义上的争论。在变量声明之前在块中引用变量总是会导致 ReferenceError,因为该变量从块开始直到声明被处理为止都处于“暂时死区”中。

js
console.log(x); // ReferenceError
const x = 3;

console.log(y); // ReferenceError
let y = 3;

var 声明不同,var 声明只提升声明而不提升其值,而函数声明是完全提升的——您可以安全地在其作用域中的任何位置调用该函数。有关更多讨论,请参阅提升词汇表条目。

全局变量

全局变量实际上是全局对象的属性。

在网页中,全局对象是 window,因此您可以使用 window.variable 语法读取和设置全局变量。在所有环境中,globalThis 变量(它本身也是一个全局变量)可用于读取和设置全局变量。这是为了在各种 JavaScript 运行时之间提供一致的接口。

因此,您可以通过指定 windowframe 名称从一个窗口或框架访问在另一个窗口或框架中声明的全局变量。例如,如果在文档中声明了一个名为 phoneNumber 的变量,您可以从 iframe 中将其引用为 parent.phoneNumber

常量

您可以使用 const 关键字创建一个只读的命名常量。常量标识符的语法与任何变量标识符相同:它必须以字母、下划线或美元符号 ($) 开头,并且可以包含字母、数字或下划线字符。

js
const PI = 3.14;

在脚本运行时,常量不能通过赋值改变值,也不能重新声明。它必须初始化为一个值。常量的作用域规则与 let 块作用域变量相同。

您不能在同一作用域中声明与函数或变量同名的常量。例如

js
// THIS WILL CAUSE AN ERROR
function f() {}
const f = 5;

// THIS WILL CAUSE AN ERROR TOO
function f() {
  const g = 5;
  var g;
}

但是,const 只阻止重新赋值,而不阻止突变。赋值给常量的对象的属性不受保护,因此以下语句可以正常执行。

js
const MY_OBJECT = { key: "value" };
MY_OBJECT.key = "otherValue";

同样,数组的内容也不受保护,因此以下语句可以正常执行。

js
const MY_ARRAY = ["HTML", "CSS"];
MY_ARRAY.push("JAVASCRIPT");
console.log(MY_ARRAY); // ['HTML', 'CSS', 'JAVASCRIPT'];

数据结构和类型

数据类型

最新的 ECMAScript 标准定义了八种数据类型

  • 七种原始数据类型

    1. 布尔值truefalse
    2. null。表示空值的特殊关键字。(因为 JavaScript 区分大小写,所以 nullNullNULL 或任何其他变体都不同。)
    3. undefined。一个顶级属性,其值未定义。
    4. 数字。整数或浮点数。例如:423.14159
    5. BigInt。任意精度的整数。例如:9007199254740992n
    6. 字符串。表示文本值的字符序列。例如:"Howdy"
    7. Symbol。一种实例唯一且不可变的数据类型。
  • 以及对象

尽管这些数据类型相对较少,但它们使您能够对应用程序执行有用的操作。函数是该语言的另一个基本元素。虽然函数在技术上是一种对象,但您可以将对象视为值的命名容器,将函数视为脚本可以执行的过程。

数据类型转换

JavaScript 是一种动态类型语言。这意味着您在声明变量时不必指定其数据类型。这也意味着数据类型在脚本执行期间会根据需要自动转换。

例如,您可以按如下方式定义变量

js
let answer = 42;

之后,您可以将相同的变量赋值为字符串值,例如

js
answer = "Thanks for all the fish!";

因为 JavaScript 是动态类型的,所以此赋值不会导致错误消息。

数字与“+”运算符

在涉及数字和字符串值与 + 运算符的表达式中,JavaScript 将数字值转换为字符串。例如,考虑以下语句

js
x = "The answer is " + 42; // "The answer is 42"
y = 42 + " is the answer"; // "42 is the answer"
z = "37" + 7; // "377"

对于所有其他运算符,JavaScript 将数字值转换为字符串。例如

js
"37" - 7; // 30
"37" * 7; // 259

将字符串转换为数字

如果代表数字的值以字符串形式存储在内存中,则有多种转换方法。

parseInt 只返回整数,因此它对小数的使用有所减少。

注意:此外,parseInt 的最佳实践是始终包含基数参数。基数参数用于指定要使用的数值系统。

js
parseInt("101", 2); // 5

从字符串中检索数字的另一种方法是使用 +(一元加)运算符。这会隐式执行数字转换,这与 Number() 函数的过程相同。

js
"1.1" + "1.1"; // '1.11.1'
(+"1.1") + (+"1.1"); // 2.2
// Note: the parentheses are added for clarity, not required.

字面量

字面量表示 JavaScript 中的值。这些是固定值(不是变量),您在脚本中字面提供。本节描述以下类型的字面量

数组字面量

数组字面量是由零个或多个表达式组成的列表,每个表达式代表一个数组元素,并用方括号 ([]) 括起来。当您使用数组字面量创建数组时,它将用指定的值初始化为其元素,并且其 length 设置为指定的参数数量。

以下示例创建了包含三个元素且 length 为三的 coffees 数组

js
const coffees = ["French Roast", "Colombian", "Kona"];

每次评估数组字面量时,它都会创建一个新的数组对象。例如,在全局作用域中用字面量定义的数组在脚本加载时创建一次。但是,如果数组字面量在函数内部,则每次调用该函数时都会实例化一个新的数组。

注意:数组字面量创建 Array 对象。有关 Array 对象的详细信息,请参阅 Array索引集合

数组字面量中的多余逗号

如果您在数组字面量中连续放置两个逗号,数组会为未指定的元素留下一个空槽。以下示例创建了 fish 数组

js
const fish = ["Lion", , "Angel"];

当您输出此数组时,您将看到

js
console.log(fish);
// [ 'Lion', <1 empty item>, 'Angel' ]

请注意,第二个项目是“empty”,这与实际的 undefined 值并不完全相同。在使用 Array.prototype.map 等数组遍历方法时,空槽会被跳过。但是,通过索引访问 fish[1] 仍然会返回 undefined

如果您在元素列表末尾包含一个尾随逗号,则该逗号将被忽略。

在以下示例中,数组的 length 为三。没有 myList[3],并且 myList[1] 为空。列表中的所有其他逗号都表示一个新元素。

js
const myList = ["home", , "school"];

在以下示例中,数组的 length 为四,并且 myList[0]myList[2] 缺失。

js
const myList = [, "home", , "school"];

在以下示例中,数组的 length 为四,并且 myList[1]myList[3] 缺失。只有最后一个逗号被忽略。

js
const myList = ["home", , "school", ,];

注意:当您有一个多行数组时,尾随逗号有助于保持 git diff 干净,因为在末尾添加一个项目只增加一行,而不会修改前一行。

diff
const myList = [
  "home",
  "school",
+ "hospital",
];

理解多余逗号的行为对于理解 JavaScript 作为一种语言至关重要。

然而,在编写自己的代码时,您应该明确声明缺失的元素为 undefined,或者至少插入注释以突出其缺失。这样做可以提高代码的清晰度和可维护性。

js
const myList = ["home", /* empty */, "school", /* empty */, ];

布尔字面量

布尔类型有两个字面值:truefalse

注意:不要将原始布尔值 truefalseBoolean 对象的真值和假值混淆。

Boolean 对象是原始布尔数据类型的包装器。有关更多信息,请参阅 Boolean

数值字面量

JavaScript 数字字面量包括不同进制的整数字面量以及十进制浮点字面量。

请注意,语言规范要求数字字面量是无符号的。尽管如此,像 -123.4 这样的代码片段是正常的,被解释为应用于数字字面量 123.4 的一元 - 运算符。

整数字面量

整数和 BigInt 字面量可以用十进制(基数 10)、十六进制(基数 16)、八进制(基数 8)和二进制(基数 2)书写。

  • 十进制整数字面量是 without 一个前导 0(零)的数字序列。
  • 整数字面量的前导 0(零),或前导 0o(或 0O)表示它是八进制。八进制整数字面量只能包含数字 07
  • 前导 0x(或 0X)表示一个十六进制整数字面量。十六进制整数可以包含数字(09)和字母 af 以及 AF。(字符的大小写不会改变其值。因此:0xa = 0xA = 100xf = 0xF = 15。)
  • 前导 0b(或 0B)表示一个二进制整数字面量。二进制整数字面量只能包含数字 01
  • 整数字面量上的尾随 n 后缀表示 BigInt 字面量。BigInt 字面量可以使用上述任何基数。请注意,不允许使用 0123n 这样的前导零八进制语法,但 0o123n 是可以的。

一些整数字面量的示例是

0, 117, 123456789123456789n             (decimal, base 10)
015, 0001, 0o777777777777n              (octal, base 8)
0x1123, 0x00111, 0x123456789ABCDEFn     (hexadecimal, "hex" or base 16)
0b11, 0b0011, 0b11101001010101010101n   (binary, base 2)

有关更多信息,请参阅词法语法参考中的数字字面量

浮点字面量

浮点字面量可以包含以下部分

  • 一个无符号十进制整数,
  • 一个小数点 (.),
  • 一个小数部分(另一个十进制数),
  • 一个指数。

指数部分是一个 eE,后跟一个整数,该整数可以带符号(前缀 +-)。浮点字面量必须至少有一个数字,并且要么有一个小数点,要么有一个 e(或 E)。

更简洁地说,语法是

[digits].[digits][(E|e)[(+|-)]digits]

例如

js
3.1415926
.123456789
3.1E+12
.1e-23

对象字面量

对象字面量是一个由零个或多个属性名及其关联值组成的列表,用花括号 ({}) 括起来。

警告:不要在语句的开头使用对象字面量!这会导致错误(或行为不如您预期),因为 { 将被解释为块的开头。

以下是一个对象字面量的示例。car 对象的第一个元素定义了一个属性 myCar,并为其赋值一个新字符串 "Saturn";第二个元素 getCar 属性立即被赋值为调用函数 (carTypes("Honda")) 的结果;第三个元素 special 属性使用了一个现有变量 (sales)。

js
const sales = "Toyota";

function carTypes(name) {
  return name === "Honda" ? name : `Sorry, we don't sell ${name}.`;
}

const car = { myCar: "Saturn", getCar: carTypes("Honda"), special: sales };

console.log(car.myCar); // Saturn
console.log(car.getCar); // Honda
console.log(car.special); // Toyota

此外,您可以将数字或字符串字面量用作属性的名称,或者将一个对象嵌套在另一个对象中。以下示例使用了这些选项。

js
const car = { manyCars: { a: "Saab", b: "Jeep" }, 7: "Mazda" };

console.log(car.manyCars.b); // Jeep
console.log(car[7]); // Mazda

对象属性名称可以是任何字符串,包括空字符串。如果属性名称不是有效的 JavaScript 标识符或数字,则必须用引号括起来。

无效标识符的属性名称不能作为点 (.) 属性访问。

js
const unusualPropertyNames = {
  "": "An empty string",
  "!": "Bang!",
};
console.log(unusualPropertyNames.""); // SyntaxError: Unexpected string
console.log(unusualPropertyNames.!); // SyntaxError: Unexpected token !

相反,它们必须使用方括号表示法 ([]) 访问。

js
console.log(unusualPropertyNames[""]); // An empty string
console.log(unusualPropertyNames["!"]); // Bang!

增强的对象字面量

对象字面量支持一系列简写语法,包括在构造时设置原型、foo: foo 赋值的简写、定义方法、进行 super 调用以及使用表达式计算属性名称。

总而言之,这些也使对象字面量和类声明更加接近,并允许基于对象的设计受益于一些相同的便利。

js
const obj = {
  // __proto__
  __proto__: theProtoObj,
  // Shorthand for 'handler: handler'
  handler,
  // Methods
  toString() {
    // Super calls
    return `d ${super.toString()}`;
  },
  // Computed (dynamic) property names
  ["prop_" + (() => 42)()]: 42,
};

正则表达式字面量

正则表达式字面量(将在稍后详细定义)是括在斜杠之间的模式。以下是正则表达式字面量的示例。

js
const re = /ab+c/;

字符串字面量

字符串字面量是零个或多个字符,用双引号 (") 或单引号 (') 括起来。字符串必须用相同类型的引号(即,要么都是单引号,要么都是双引号)分隔。

以下是字符串字面量的示例

js
'foo'
"bar"
'1234'
'one line \n another line'
"Joyo's cat"

除非您特别需要使用 String 对象,否则应使用字符串字面量。有关 String 对象的详细信息,请参阅 String

您可以对字符串字面量值调用 String 对象的任何方法。JavaScript 会自动将字符串字面量转换为临时 String 对象,调用方法,然后丢弃临时 String 对象。您还可以将 length 属性与字符串字面量一起使用

js
// Will print the number of symbols in the string including whitespace.
console.log("Joyo's cat".length); // In this case, 10.

模板字面量也可用。模板字面量用反引号 (`)(重音符)字符而不是双引号或单引号括起来。

模板字面量为构建字符串提供了语法糖。(这类似于 Perl、Python 等语言中的字符串插值功能。)

js
// Basic literal string creation
`In JavaScript '\n' is a line-feed.`;

// Multiline strings
`In JavaScript, template strings can run
 over multiple lines, but double and single
 quoted strings cannot.`;

// String interpolation
const name = "Lev",
  time = "today";
`Hello ${name}, how are you ${time}?`;

带标签的模板是一种紧凑的语法,用于指定模板字面量以及调用“标签”函数来解析它。带标签的模板只是一种更简洁、更有语义的方式来调用一个处理字符串和一组相关值的函数。模板标签函数的名称在模板字面量之前——如下例所示,其中模板标签函数名为 printprint 函数将插入参数并序列化可能出现的任何对象或数组,从而避免令人讨厌的 [object Object]

js
const formatArg = (arg) => {
  if (Array.isArray(arg)) {
    // Print a bulleted list
    return arg.map((part) => `- ${part}`).join("\n");
  }
  if (arg.toString === Object.prototype.toString) {
    // This object will be serialized to "[object Object]".
    // Let's print something nicer.
    return JSON.stringify(arg);
  }
  return arg;
};

const print = (segments, ...args) => {
  // For any well-formed template literal, there will always be N args and
  // (N+1) string segments.
  let message = segments[0];
  segments.slice(1).forEach((segment, index) => {
    message += formatArg(args[index]) + segment;
  });
  console.log(message);
};

const todos = [
  "Learn JavaScript",
  "Learn Web APIs",
  "Set up my website",
  "Profit!",
];

const progress = { javascript: 20, html: 50, css: 10 };

print`I need to do:
${todos}
My current progress is: ${progress}
`;

// I need to do:
// - Learn JavaScript
// - Learn Web APIs
// - Set up my website
// - Profit!
// My current progress is: {"javascript":20,"html":50,"css":10}

由于带标签的模板字面量只是函数调用的语法糖,您可以将上述内容重写为等效的函数调用

js
print(["I need to do:\n", "\nMy current progress is: ", "\n"], todos, progress);

这可能让人联想到 console.log 风格的插值

js
console.log("I need to do:\n%o\nMy current progress is: %o\n", todos, progress);

您可以看到带标签的模板比传统的“格式化器”函数更自然,在后者中,变量和模板本身必须单独声明。

在字符串中使用特殊字符

除了普通字符之外,您还可以在字符串中包含特殊字符,如下例所示。

js
"one line \n another line";

下表列出了您可以在 JavaScript 字符串中使用的特殊字符。

字符 含义
\0 空字节
\b 退格键
\f 换页符
\n 换行符
\r 回车符
\t 制表符
\v 垂直制表符
\' 撇号或单引号
\" 双引号
\\ 反斜杠字符
\XXX 由最多三个八进制数字 XXX(介于 0377 之间)指定的带有 Latin-1 编码的字符。例如,\251 是版权符号的八进制序列。
\xXX 由两个十六进制数字 XX(介于 00FF 之间)指定的带有 Latin-1 编码的字符。例如,\xA9 是版权符号的十六进制序列。
\uXXXX 由四个十六进制数字 XXXX 指定的 Unicode 字符。例如,\u00A9 是版权符号的 Unicode 序列。请参阅Unicode 转义序列
\u{XXXXX} Unicode 码点转义。例如,\u{2F804} 与 Unicode 转义 \uD87E\uDC04 相同。

转义字符

对于表中未列出的字符,前导反斜杠将被忽略,但这种用法已弃用,应避免使用。

您可以通过在引号前加上反斜杠来在字符串中插入引号。这被称为转义引号。例如

js
const quote = "He read \"The Cremation of Sam McGee\" by R.W. Service.";
console.log(quote);

结果将是

He read "The Cremation of Sam McGee" by R.W. Service.

要在字符串中包含字面反斜杠,您必须转义反斜杠字符。例如,要将文件路径 c:\temp 赋值给字符串,请使用以下内容

js
const home = "c:\\temp";

您还可以通过在换行符前加上反斜杠来转义换行符。反斜杠和换行符都从字符串的值中移除。

js
const str =
  "this string \
is broken \
across multiple \
lines.";
console.log(str); // this string is broken across multiple lines.

更多信息

本章侧重于声明和类型的基本语法。要了解更多关于 JavaScript 语言结构的信息,另请参阅本指南中的以下章节

在下一章中,我们将了解控制流结构和错误处理。