4.语法

4.1.1. 基本语法

注释:

// 单行注释
/*
  多行
  注释
*/

原始 (原子)值:

// Booleans
true
false
// Numbers (JavaScript 的数字只有一种类型)
-123
1.414
// String (JavaScript 的字符没有对应的类型)
'abc'
"abc"

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

assert.equal(7 + 1, 8);

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

打印日志到 浏览器的控制台 或 Node.js:

// 将值标准输出打印(另一种方法调用)
console.log("Hello!");
// 将错误信息标准输出打印
console.error('Something weng wrong!');

运算符:

// 布尔运算符
assert.equal(true && false, false); // 与
assert.equal(true || false, true); // 或
// 数学运算符
assert.equal(3 + 4, 7);
assert.equal(5 - 1, 4);
assert.equal(3 * 4, 12);
assert.equal(9 / 3, 3);
// 字符串操作符
assert.equal('a' + 'b', 'ab');
assert.equal('I see ' + 3 + ' monkeys', 'I see 3 monkeys');
// 比较运算符
assert.equal(3 < 4, true);
assert.equal(3 <= 4, true);
assert.equal('abc' === 'abc', true);
assert.equal('abc' !== 'def', true);

声明变量:

let x;  // 声明 x(可变的)
x = 3 * 5;  // 给 x 赋值
let y = 3 * 5; // 声明变量并为其赋值
const z = 8; // 声明 y(不可变的)

控制流声明:

// 条件语句
if (x < 0) { // x 小于 0?
  x = -x;
}

普通函数声明:

// add1() 有 a 和 b 两个参数
function add1(a, b) {
  return a + b;
}
// 调用函数 add1()
assert.equal(add1(5, 2), 7);

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

const add2 = (a, b) => a + b;
// 调用方法 add2()
assert.equal(add2(5, 2), 7);
const add3 = (a, b) => { return a + b };

上面的代码包含以下箭头函数:

// 主体为一个表达式的箭头函数
(a, b) => a + b
// 主体为一个代码块的箭头函数
(a, b) => { return a + b }

对象:

// 通过 object 符号({})创建一个对象
const obj = {
  first: 'Jane', // 属性
  last: 'Doe', // 属性
  getFullName() { // 属性(方法)
    return this.first + ' ' + this.last;
  },
};
// 获取某个属性值
assert.equal(obj.first, 'Jane');
// 设置某个属性的值
obj.first = 'Janey';
// 调用对象的方法
assert.equal(obj.getFullName(), 'Janey Doe');

数组(数组也是对象):

// 通过 Array 符号([])创建一个数组
const arr = ['a', 'b', 'c'];
// 获取某个数组元素
assert.equal(arr[1], 'b');
// 设置某个数组元素的值
arr[1] = 'β';

4.1.2. 模块

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

file-tools.js
main.js

file-tools.js中的模块导出其功能isTextFilePath()

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

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

// 导入整个模块 `path`
import * as path from 'path';
// 导入模块 file-tools.js export的一个函数
import {isTextFilePath} from './file-tools.js';

4.1.3. 法定变量和属性名称

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

标识符允许具有以下字符:

  • Unicode 字母:A - Za - z(等)
  • $_
  • Unicode 数字:0 - 9(等)
  • 变量名不能以数字开头

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

保留字不能用作变量名:

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

但它们被允许作为属性的名称:

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

4.1.4. 连接类型

用于连接单词的常见类型是:

  • 驼峰:threeConcatenatedWords
  • 下划线(也称蛇形 ):three_concatenated_words
  • 分隔线(也称串形 ):three-concatenated-words

4.1.5. 命名大写

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

小写:

  • 函数,变量:myFunction
  • 方法:obj.myMethod
  • CSS:
    • CSS 实体:special-class
    • 对应的 JavaScript 变量:specialClass

大写:

  • 类名:MyClass
  • 常数:MY_CONSTANT
    • 常量也常用骆驼写成:myConstant

4.1.6. 在哪里加分号?

在语句的最后:

const x = 123;
func();

但是,如果该语句以大括号结尾,那就不加分号了:

while (false) {
  // ···
} // 无分号
function func() {
  // ···
} // 无分号

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

// 函数声明后跟空语句
function func() {
  // ···
};

4.2. 深入

本章的所有其余部分都是深入说明(上面的内容)。

4.3. 身份标识

4.3.1. 有效标识符(变量名等)

首字符:

  • Unicode 字母(包括éü等重音字符和非拉丁字母的字符,如α
  • $
  • _

后续字符:

  • 合法的首字符
  • Unicode 数字(包括东方阿拉伯数字)
  • 一些其他 Unicode 标记和标点符号

例如:

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

4.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等)用于您自己的变量和参数。

4.4. 语句与表达

在本节中,我们将探讨 JavaScript 如何区分两种句法结构:_ 语句 _ 和 _ 表达式 _。之后,我们会发现这会导致问题,因为相同的语法可能意味着不同的东西,具体取决于它的使用位置。

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

4.4.1. 语句

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

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

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

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

4.4.2. 表达式

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

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

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

让我们看一下表达式的更多例子。我们输入表达式,REPL 为我们评估它们:

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

4.4.3. 什么是允许的?

JavaScript 源代码中的当前位置决定了您可以使用哪种语法结构:

  • 函数体必须是一系列语句:
    function max(x, y) {
      if (x > y) {
        return x;
      } else {
        return y;
      }
    }
    
  • 函数调用或方法调用的参数必须是表达式:
    console.log('ab' + 'cd', Number('123'));
    

但是,表达式可以用作语句。然后将它们称为表达式语句 。相反的情况并非如此:当上下文需要表达式时,你便不能使用语句。

以下代码演示了某个表达式bar()可以是表达式还是语句——它取决于上下文:

console.log(bar());
bar();

4.5. 语法模糊

JavaScript 有几种语法歧义的编程结构:相同的语法被不同地解释,这取决于它是在语句上下文中还是在表达式上下文中使用。本节探讨了这一现象及其引发的陷阱。

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

函数声明 是一个声明:

function id(x) {
  return x;
}

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

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

4.5.2. 相同的语法:object literal 和 block

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

const obj = {};

这是一个空代码块(声明):

{
}

4.5.3. 消除歧义

歧义只是语句上下文中的一个问题:如果 JavaScript 解析器遇到模糊语法,它不知道它是简单语句还是表达式语句。例如:

  • 如果语句以function开头:它是函数声明还是函数表达式?
  • 如果语句以{开头:它是对象字面值还是代码块?

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

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

在这段代码中:

  1. 我们首先通过函数表达式创建一个函数:
    function (x) { console.log(x) }
    
  2. 然后我们调用该函数:('abc')

#1 只被解释为表达式,因为我们将它包装在括号中。如果我们没有,我们会得到一个语法错误,因为 JavaScript 需要一个函数声明,之后还会警告缺少的函数名称。此外,你不能在函数声明后立即进行函数调用。

在本书的后面,我们将看到更多由语法模糊引起的陷阱的例子:

4.6. 分号

4.6.1. 分号的经验法则

每个语句都以分号结束。

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

例外:以块结尾的语句。

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

以下情况有点棘手:

const func = () => {}; // 分号!

整个const声明(一个语句)以分号结尾,但在其中,有一个箭头函数表达式。那就是:声明本身并不是以花括号结尾;它是嵌入式箭头函数表达式。这就是为什么最后会有一个分号的原因。

4.6.2. 分号:控制语句

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

while (condition)
  语句

正文可以是一行语句:

while (a > 0) a--;

但代码块也是声明,因此也是控制声明的合法主体:

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

如果你想让一个循环有一个空的主体,那么你的首选便是一个空语句(它只是一个分号):

while (processNextItem() > 0);

你的第二个选择是一个空语句块:

while (processNextItem() > 0) {}

4.7. 自动分号插入(ASI)

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

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

  • 分号
  • 行终止符后跟非法标记

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

4.7.1. ASI 意外触发

关于 ASI 的好消息是——如果你不依赖它并且总是写分号——你只需要注意一个陷阱。这是 JavaScript 禁止在一些标记之后的换行符。如果插入换行符,也会插入分号。

最实际相关的标记是return。例如,考虑以下代码:

return
{
  first: 'jane'
};

此代码解析为:

return;
{
  first: 'jane';
}
;

也就是说,一个空的 return 语句,后跟一个代码块,后跟一个空语句。

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

4.7.2. ASI 意外没有触发

在某些情况下,当您认为 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');
assert.equal(propKey, 'ol'); // 因为逗号
someFunction()[propKey].map(x => x + x);

4.8. 分号:最佳实践

我建议你要经常写分号:

  • 我喜欢它提供代码的视觉结构——你清楚地看到一个语句何时结束。
  • 要记住的规则较少。
  • 大多数 JavaScript 程序员使用分号。

然而,也有许多人不喜欢添加分号的视觉混乱。如果你是其中之一:可能会认为没有它们的代码亦是 合法的。我建议你使用工具来帮助您避免错误。以下是两个例子:

  • 自动代码格式化程序 Prettier 可以配置为不使用分号。然后它会自动修复问题。例如,如果它遇到以方括号开头的行,则它以分号为前缀。
  • 静态检查器 ESLint 有一套你可以表述你的首选样式的规则(始终使用分号或尽可能少的分号),并向你对关键问题报警。

4.9. 严格模式

从 ECMAScript 5 开始,你可以选择在所谓的严格模式 中执行 JavaScript。在该模式下,语言稍微清晰:不存在一些怪异的写法并且同时会抛出更多异常。

默认(非严格)模式也称为草率模式

请注意,默认模式在模块和类中默认打开,因此在编写现代 JavaScript(几乎总是位于模块中)时,您并不需要了解它。在本书中,我假设严格模式始终打开。

4.9.1. 开启严格模式

在旧脚本文件和 CommonJS 模块中,通过将以下代码放在第一行中,您可以为完整文件切换严格模式:

'use strict';

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

你还可以仅为单个函数打开严格模式:

function functionInStrictMode() {
  'use strict';
}

4.9.2. 示例:严格模式

让我们看一个示例,其中草率模式做一些严格模式不会做的坏事:更改未知变量(未通过let或类似创建)创建一个全局变量。

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

严格模式则做得更好:

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

assert.throws()要求它的第一个参数,某个函数,在调用时抛出ReferenceError