语法和类型

本章讨论 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

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

声明

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声明不同,函数声明会被完整提升——你可以在其作用域的任何地方安全地调用该函数。有关更多讨论,请参阅提升术语表条目。

全局变量

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

在网页中,全局对象是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

另一种从字符串中获取数字的方法是使用+(一元加号)运算符

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' ]

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

如果在元素列表的末尾包含尾随逗号,则会忽略该逗号。

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

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差异的整洁,因为将项目追加到末尾只会添加一行,而不会修改上一行。

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

了解额外逗号的行为对于理解JavaScript作为一种语言非常重要。

但是,在编写自己的代码时,你应该显式地将缺失的元素声明为undefined,或者至少插入注释以突出显示其缺失。这样做可以提高代码的清晰度和可维护性。

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

布尔字面量

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

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

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

数字字面量

JavaScript数字字面量包括不同基数的整数字面量以及以10为基数的浮点字面量。

请注意,语言规范要求数字字面量为无符号数。但是,像-123.4这样的代码片段是可以的,将其解释为应用于数字字面量123.4的一元-运算符。

整数字面量

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

  • 十进制整数字面量是数字序列,没有前导0(零)。
  • 整数字面量的前导0(零)或前导0o(或0O)表示它是八进制的。八进制整数字面量只能包含数字07
  • 前导0x(或0X)表示十六进制整数字面量。十六进制整数可以包含数字(09)以及字母afAF。(字符的大小写不会改变其值。因此: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语言结构的更多信息,还可以参阅本指南中的以下章节

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