面向急切程序员的 JavaScript(ES2022 版)
请支持本书:购买捐赠
(广告,请勿屏蔽。)

7 语法



7.1 JavaScript 语法概述

这是对 JavaScript 语法的初步了解。如果有些东西现在还不太明白,请不要担心。本书稍后将更详细地解释所有内容。

此外,本概述并不详尽。它侧重于基本要素。

7.1.1 基本结构

7.1.1.1 注释
// single-line comment

/*
Comment with
multiple lines
*/
7.1.1.2 *原始*(原子)值

布尔值

true
false

数字

1.141
-123

基本数字类型用于浮点数(双精度)和整数。

BigInt

17n
-49n

基本数字类型只能正确表示 53 位加符号范围内的整数。BigInt 的大小可以任意增大。

字符串

'abc'
"abc"
`String with interpolated values: ${256} and ${true}`

JavaScript 没有用于字符的额外类型。它使用字符串来表示它们。

7.1.1.3 断言

*断言*描述了计算结果的预期形式,如果这些预期不正确,则会抛出异常。例如,以下断言表明 7 加 1 的计算结果必须为 8

assert.equal(7 + 1, 8);

assert.equal() 是一个方法调用(对象是 assert,方法是 .equal()),它有两个参数:实际结果和预期结果。它是 Node.js 断言 API 的一部分,本书稍后将对此进行解释

还有 assert.deepEqual(),它可以深度比较对象。

7.1.1.4 记录到控制台

记录到浏览器或 Node.js 的控制台

// Printing a value to standard out (another method call)
console.log('Hello!');

// Printing error information to standard error
console.error('Something went wrong!');
7.1.1.5 运算符
// Operators for booleans
assert.equal(true && false, false); // And
assert.equal(true || false, true); // Or

// Operators for numbers
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(10 / 4, 2.5);

// Operators for bigints
assert.equal(3n + 4n, 7n);
assert.equal(5n - 1n, 4n);
assert.equal(3n * 4n, 12n);
assert.equal(10n / 4n, 2n);

// Operators for strings
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');

// Comparison operators
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);

JavaScript 也有一个 == 比较运算符。我建议避免使用它 - 原因在§13.4.3 “建议:始终使用严格相等”中解释。

7.1.1.6 声明变量

const 创建*不可变变量绑定*:每个变量都必须立即初始化,并且我们以后不能再分配不同的值。但是,值本身可能是可变的,我们也许能够更改其内容。换句话说:const 不会使值不可变。

// Declaring and initializing x (immutable binding):
const x = 8;

// Would cause a TypeError:
// x = 9;

let 创建*可变变量绑定*

// Declaring y (mutable binding):
let y;

// We can assign a different value to y:
y = 3 * 5;

// Declaring and initializing z:
let z = 3 * 5;
7.1.1.7 普通函数声明
// add1() has the parameters a and b
function add1(a, b) {
  return a + b;
}
// Calling function add1()
assert.equal(add1(5, 2), 7);
7.1.1.8 箭头函数表达式

箭头函数表达式特别用作函数调用和方法调用的参数

const add2 = (a, b) => { return a + b };
// Calling function add2()
assert.equal(add2(5, 2), 7);

// Equivalent to add2:
const add3 = (a, b) => a + b;

前面的代码包含以下两个箭头函数(术语*表达式*和*语句*将在本章后面解释)

// An arrow function whose body is a code block
(a, b) => { return a + b }

// An arrow function whose body is an expression
(a, b) => a + b
7.1.1.9 普通对象
// Creating a plain object via an object literal
const obj = {
  first: 'Jane', // property
  last: 'Doe', // property
  getFullName() { // property (method)
    return this.first + ' ' + this.last;
  },
};

// Getting a property value
assert.equal(obj.first, 'Jane');
// Setting a property value
obj.first = 'Janey';

// Calling the method
assert.equal(obj.getFullName(), 'Janey Doe');
7.1.1.10 数组
// Creating an Array via an Array literal
const arr = ['a', 'b', 'c'];
assert.equal(arr.length, 3);

// Getting an Array element
assert.equal(arr[1], 'b');
// Setting an Array element
arr[1] = 'β';

// Adding an element to an Array:
arr.push('d');

assert.deepEqual(
  arr, ['a', 'β', 'c', 'd']);
7.1.1.11 控制流语句

条件语句

if (x < 0) {
  x = -x;
}

for-of 循环

const arr = ['a', 'b'];
for (const element of arr) {
  console.log(element);
}
// Output:
// 'a'
// 'b'

7.1.2 模块

每个模块都是一个单独的文件。例如,考虑以下两个包含模块的文件

file-tools.mjs
main.mjs

file-tools.mjs 中的模块导出其函数 isTextFilePath()

export function isTextFilePath(filePath) {
  return filePath.endsWith('.txt');
}

main.mjs 中的模块导入整个模块 path 和函数 isTextFilePath()

// Import whole module as namespace object `path`
import * as path from 'path';
// Import a single export of module file-tools.mjs
import {isTextFilePath} from './file-tools.mjs';

7.1.3 类

class Person {
  constructor(name) {
    this.name = name;
  }
  describe() {
    return `Person named ${this.name}`;
  }
  static logNames(persons) {
    for (const person of persons) {
      console.log(person.name);
    }
  }
}

class Employee extends Person {
  constructor(name, title) {
    super(name);
    this.title = title;
  }
  describe() {
    return super.describe() +
      ` (${this.title})`;
  }
}

const jane = new Employee('Jane', 'CTO');
assert.equal(
  jane.describe(),
  'Person named Jane (CTO)');

7.1.4 异常处理

function throwsException() {
  throw new Error('Problem!');
}

function catchesException() {
  try {
    throwsException();
  } catch (err) {
    assert.ok(err instanceof Error);
    assert.equal(err.message, 'Problem!');
  }
}

注意

变量名和属性名的语法类别称为*标识符*。

标识符允许包含以下字符

有些词在 JavaScript 中具有特殊含义,称为*保留字*。例如:iftrueconst

保留字不能用作变量名

const if = 123;
  // SyntaxError: Unexpected token if

但它们可以用作属性名

> const obj = { if: 123 };
> obj.if
123

7.1.6 大小写风格

用于连接单词的常见大小写风格有

7.1.7 名称的大小写

通常,JavaScript 使用驼峰式大小写,但常量除外。

小写

大写

7.1.8 更多命名约定

以下命名约定在 JavaScript 中很流行。

如果参数名称以下划线开头(或为下划线),则表示未使用此参数 - 例如

arr.map((_x, i) => i)

如果对象属性的名称以下划线开头,则该属性被视为私有属性

class ValueWrapper {
  constructor(value) {
    this._value = value;
  }
}

7.1.9 分号放在哪里?

在语句的末尾

const x = 123;
func();

但如果该语句以花括号结尾,则不需要

while (false) {
  // ···
} // no semicolon

function func() {
  // ···
} // no semicolon

但是,在这样的语句后面添加分号并不是语法错误 - 它被解释为空语句

// Function declaration followed by empty statement:
function func() {
  // ···
};

  测验:基础

请参阅测验应用程序

7.2 (高级)

本章的其余部分均为高级内容。

7.3 标识符

7.3.1 有效的标识符(变量名等)

第一个字符

后续字符

示例

const ε = 0.0001;
const строка = '';
let _tmp = 0;
const $foo2 = true;

7.3.2 保留字

保留字不能用作变量名,但可以用作属性名。

所有 JavaScript *关键字*都是保留字:

await break case catch class const continue debugger default delete do else export extends finally for function if import in instanceof let new return static super switch this throw try typeof var void while with yield

以下标记也是关键字,但目前在语言中未使用

enum implements package protected interface private public

以下字面量是保留字

true false null

从技术上讲,这些词不是保留字,但您也应该避免使用它们,因为它们实际上是关键字

Infinity NaN undefined async

您也不应该将全局变量的名称(StringMath 等)用于您自己的变量和参数。

7.4 语句与表达式

在本节中,我们将探讨 JavaScript 如何区分两种语法结构:*语句*和*表达式*。之后,我们将看到这可能会导致问题,因为相同的语法在不同的使用位置可能意味着不同的含义。

  我们假设只有语句和表达式

为了简单起见,我们假设 JavaScript 中只有语句和表达式。

7.4.1 语句

*语句*是可以执行并执行某种操作的代码段。例如,if 是一个语句

let myStr;
if (myBool) {
  myStr = 'Yes';
} else {
  myStr = 'No';
}

另一个语句的例子:函数声明。

function twice(x) {
  return x + x;
}

7.4.2 表达式

*表达式*是可以*求值*以产生值的代码段。例如,括号之间的代码是一个表达式

let myStr = (myBool ? 'Yes' : 'No');

括号之间使用的运算符 _?_:_ 称为*三元运算符*。它是 if 语句的表达式版本。

让我们看看更多表达式的例子。我们输入表达式,REPL 会为我们对其进行求值

> 'ab' + 'cd'
'abcd'
> Number('123')
123
> true || false
true

7.4.3 哪些地方允许使用什么?

JavaScript 源代码中的当前位置决定了允许使用哪种语法结构

但是,表达式可以用作语句。然后它们被称为*表达式语句*。反之则不然:当上下文需要表达式时,不能使用语句。

以下代码演示了任何表达式 bar() 都可以是表达式或语句 - 这取决于上下文

function f() {
  console.log(bar()); // bar() is expression
  bar(); // bar(); is (expression) statement  
}

7.5 歧义语法

JavaScript 中有几种语法结构具有歧义:相同的语法在语句上下文和表达式上下文中解释不同。本节将探讨这种现象及其造成的陷阱。

7.5.1 相同的语法:函数声明和函数表达式

*函数声明*是一个语句

function id(x) {
  return x;
}

*函数表达式*是一个表达式(= 的右侧)

const id = function me(x) {
  return x;
};

7.5.2 相同的语法:对象字面量和代码块

在以下代码中,{} 是一个*对象字面量*:一个创建空对象的表达式。

const obj = {};

这是一个空的代码块(一个语句)

{
}

7.5.3 消除歧义

歧义只在语句上下文中才会成为问题:如果 JavaScript 解析器遇到歧义语法,它就不知道它是普通语句还是表达式语句。例如

为了消除歧义,以 function{ 开头的语句永远不会被解释为表达式。如果希望表达式语句以这两个标记中的任何一个开头,则必须将其括在括号中

(function (x) { console.log(x) })('abc');

// Output:
// 'abc'

在此代码中

  1. 我们首先通过函数表达式创建一个函数

    function (x) { console.log(x) }
  2. 然后我们调用该函数:('abc')

代码片段 (1) 中显示的内容仅被解释为表达式,因为我们将其包装在括号中。如果我们不这样做,我们会得到一个语法错误,因为 JavaScript 需要一个函数声明,并会抱怨缺少函数名。此外,您不能在函数声明后立即放置函数调用。

在本书的后面,我们将看到更多由语法歧义引起的陷阱的例子。

7.6 分号

7.6.1 分号使用规则

每个语句都以分号结尾

const x = 3;
someFunction('abc');
i++;

以代码块结尾的语句除外

function foo() {
  // ···
}
if (y > 0) {
  // ···
}

以下情况稍微有些棘手

const func = () => {}; // semicolon!

整个 const 声明(一个语句)以分号结尾,但在其内部,有一个箭头函数表达式。也就是说,并不是语句本身以花括号结尾;而是嵌入的箭头函数表达式。这就是为什么最后有一个分号。

7.6.2 分号:控制语句

控制语句的主体本身就是一个语句。例如,这是 while 循环的语法

while (condition)
  statement

主体可以是单个语句

while (a > 0) a--;

但代码块也是语句,因此也是控制语句的合法主体

while (a > 0) {
  a--;
}

如果您希望循环有一个空主体,您的第一个选择是空语句(只是一个分号)

while (processNextItem() > 0);

您的第二个选择是空代码块

while (processNextItem() > 0) {}

7.7 自动分号插入 (ASI)

虽然我建议始终编写分号,但在 JavaScript 中,大多数分号都是可选的。使之成为可能的机制称为*自动分号插入* (ASI)。在某种程度上,它可以纠正语法错误。

ASI 的工作原理如下。语句的解析会一直持续到出现以下任一情况:

换句话说,可以将 ASI 视为在换行符处插入分号。接下来的几节将介绍 ASI 的陷阱。

7.7.1 ASI 意外触发

关于 ASI 的好消息是,如果您不依赖它并始终编写分号,那么您只需要注意一个陷阱。那就是 JavaScript 禁止在某些标记后换行。如果您确实插入了换行符,也会插入分号。

与之最相关的标记是 return。例如,请考虑以下代码

return
{
  first: 'jane'
};

这段代码被解析为

return;
{
  first: 'jane';
}
;

也就是说

为什么 JavaScript 要这样做?它可以防止在 return 之后的行中意外返回一个值。

7.7.2 ASI 未按预期触发

在某些情况下,当您认为应该触发 ASI 时,它并*没有*被触发。这使得不喜欢分号的人的生活更加复杂,因为他们需要注意这些情况。以下是三个例子。还有更多。

**示例 1:**意外的函数调用。

a = b + c
(d + e).print()

解析为

a = b + c(d + e).print();

**示例 2:**意外的除法。

a = b
/hi/g.exec(c).map(d)

解析为

a = b / hi / g.exec(c).map(d);

**示例 3:**意外的属性访问。

someFunction()
['ul', 'ol'].map(x => x + x)

执行为

const propKey = ('ul','ol'); // comma operator
assert.equal(propKey, 'ol');

someFunction()[propKey].map(x => x + x);

7.8 分号:最佳实践

我建议您始终编写分号

然而,也有很多人不喜欢分号带来的额外的视觉混乱。如果您是其中之一:没有它们,代码*是*合法的。我建议您使用工具来帮助您避免错误。以下是两个例子

7.9 严格模式与宽松模式

从 ECMAScript 5 开始,JavaScript 有两种可以执行 JavaScript 的*模式*

在现代 JavaScript 代码中,您很少会遇到宽松模式,这些代码几乎总是位于模块中。在本书中,我假设始终开启严格模式。

7.9.1 开启严格模式

在脚本文件和 CommonJS 模块中,您可以通过将以下代码放在第一行来为整个文件开启严格模式

'use strict';

这种“指令”的巧妙之处在于,5 之前的 ECMAScript 版本会直接忽略它:它是一个什么也不做的表达式语句。

您也可以只为单个函数开启严格模式

function functionInStrictMode() {
  'use strict';
}

7.9.2 严格模式的改进

让我们来看看严格模式比宽松模式做得更好的三件事。仅在本节中,所有代码片段都在宽松模式下执行。

7.9.2.1 宽松模式陷阱:更改未声明的变量会创建一个全局变量

在非严格模式下,更改未声明的变量会创建一个全局变量。

function sloppyFunc() {
  undeclaredVar1 = 123;
}
sloppyFunc();
// Created global variable `undeclaredVar1`:
assert.equal(undeclaredVar1, 123);

严格模式做得更好,它会抛出一个 ReferenceError。这使得检测拼写错误更容易。

function strictFunc() {
  'use strict';
  undeclaredVar2 = 123;
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'undeclaredVar2 is not defined',
  });

assert.throws() 表示它的第一个参数(一个函数)在被调用时会抛出一个 ReferenceError

7.9.2.2 函数声明在严格模式下是块级作用域,在宽松模式下是函数级作用域

在严格模式下,通过函数声明创建的变量仅存在于最内层的封闭块中

function strictFunc() {
  'use strict';
  {
    function foo() { return 123 }
  }
  return foo(); // ReferenceError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'ReferenceError',
    message: 'foo is not defined',
  });

在宽松模式下,函数声明是函数级作用域的

function sloppyFunc() {
  {
    function foo() { return 123 }
  }
  return foo(); // works
}
assert.equal(sloppyFunc(), 123);
7.9.2.3 宽松模式在更改不可变数据时不会抛出异常

在严格模式下,如果您尝试更改不可变数据,则会收到异常

function strictFunc() {
  'use strict';
  true.prop = 1; // TypeError
}
assert.throws(
  () => strictFunc(),
  {
    name: 'TypeError',
    message: "Cannot create property 'prop' on boolean 'true'",
  });

在宽松模式下,赋值会静默失败

function sloppyFunc() {
  true.prop = 1; // fails silently
  return true.prop;
}
assert.equal(sloppyFunc(), undefined);

  延伸阅读:宽松模式

有关宽松模式与严格模式区别的更多信息,请参阅 MDN

  测验:高级

请参阅测验应用程序