超级React教程,第 1 章:现代 JavaScript

总目录

JavaScript语言在过去几年中发生了重大变化,但由于浏览器在采用这些变化方面进展缓慢,因此很多人没有跟上该语言的步伐。React 鼓励开发人员使用现代 JavaScript,因此本章概述了该语言的最新功能。

ES5 与 ES6 的对比

JavaScript 语言规范由 ECMA 管理,ECMA 是一个非营利组织,负责维护该语言的标准化版本,称为 ECMAScript

您可能在 JavaScript 语言版本的上下文中听说过术语“ES5”和“ES6”。这些分别指 ECMAScript 标准的第 5 版和第 6 版。该语言的ES5版本于2009年发布,目前被认为是基准实施,在桌面和移动设备上得到广泛支持。ES6于2015年发布,比ES5引入了重大改进,并保持向后兼容。自ES6发布以来,ECMA每年都对标准进行修订,以继续改进和现代化该语言。在许多情况下,“ES6”名称被松散地用于ES5之后对语言的所有改进,而不是严格意义上的ES6规范。

Web浏览器如何跟上发展如此迅速的语言?他们实际上不能,也没有!在ES6中引入的功能以及后来对标准的更新并不能保证在所有浏览器中实现。为了避免代码因缺少语言特性而无法运行,现代 JavaScript 框架依赖于一种称为转译的技术,该技术将现代 JavaScript 源代码转换为功能等效的 ES5 代码,这些代码可以在任何地方运行。由于发生了转译,JavaScript开发人员不必担心JavaScript语言浏览器的哪些部分支持。

最近脚本功能摘要

本书中的代码是用现代的 JavaScript 编写的。假设您熟悉该语言的 ES5 版本,则可以使用以下部分来更新您对该语言较新部分的知识,并填补您可能遇到的任何空白。

分号

关于何时在 JavaScript 中需要分号的规则令人困惑。JavaScript 编译器在某些情况下采用隐式分号,但在其他情况下则不然。在实践中,这意味着在大多数情况下,不需要键入分号。使事情变得复杂的是,在某些情况下,它们仍然是需要的。

为了避免分号规则产生的混淆,对于本书,我决定在所有语句之后使用显式分号。我不在函数声明或控制结构(如循环或条件)的结尾处的结尾处使用分号。为什么会有这些例外?使用分号作为语句分隔符的大多数其他语言在右大括号之后不需要它。}

以下是一些示例:

const a = 1;  // <-- semicolon here

function f() {
  console.log('this is f');  // <-- semicolon here
}  // <-- but not here

将箭头函数分配给变量或常量时,会出现一个有趣的情况。我认为这是上述例外规则的例外,因此在这种情况下我使用分号:

const f = () => {
  console.log('this is f');
};  // <-- this is an assignment, so a semicolon is used

不言而喻,如果您不喜欢我的分号选择,则不需要采用它们。如果您已经对使用或省略分号产生了个人偏好,那么您绝对可以使用它来代替我自己的分号。

尾随逗号

定义跨多行的对象或数组时,在最后一个元素后保留逗号很有用。请看以下示例:

const myArray = [
  1,
  3,
  5,
];

const myObject = {
  name: 'susan',
  age: 20,
};

数组和对象的最后一个元素后面的逗号乍一看可能看起来像语法错误,但它们是有效的。事实上,JavaScript在允许这样做方面并不是唯一的,因为大多数其他语言也支持尾随逗号。

这种做法有两个好处。最重要的一个是,如果您需要对元素重新排序,您只需上下移动行,而不必担心必须修复逗号。另一个是,当您需要在末尾添加元素时,您无需向上转到上一行即可添加尾随逗号。

进出口

如果您习惯于为浏览器编写旧式JavaScript应用程序,则可能永远不需要从其他模块“导入”函数或对象。您只需添加了加载依赖项的标记,这就足以将所需的内容引入全局范围,在当前页面上下文中运行的任何 JavaScript 代码都可以访问全局范围。<script>

由于工具已集成到现代 JavaScript 前端框架中,应用程序现在可以使用基于导入导出的更合理的依赖关系模型。

想要使函数或变量可供其他模块使用的 JavaScript 模块可以将其声明为默认导出。假设有一个很酷.js模块。以下是编写此模块的方法:myCoolFunction()

export default function myCoolFunction() {
  console.log('this is cool!');
}

然后,任何其他想要使用该函数的模块都可以导入它:

import myCoolFunction from './cool';

在此导入中, 是依赖模块的路径,相对于导入文件的位置。该路径可以根据需要在目录层次结构中向上或向下导航。扩展名可以包含在导入文件名中,但它是可选的。./cool.js

使用默认导出时,导出符号的名称并不重要。导入模块可以使用它喜欢的任何名称。下一个示例也是有效的:

import myReallyCoolFunction from './cool';

从第三方库导入的工作方式类似,但导入位置使用库名称而不是本地路径。例如,下面介绍了如何导入对象:React

import React from 'react';

一个模块只能有一个默认导出,但它也可以导出其他内容。以下是上述酷.js模块的扩展,其中包含几个导出的常量(您将在下一节中了解有关常量的更多信息):

export const PI = 3.14;
export const SQRT2 = 1.41;

export default function myCoolFunction() {
  console.log('this is cool!');
}

要导入非默认导出,导入的符号必须括在 大括号中:{}

import { SQRT2 } from './cool';

此语法还允许在同一行中进行多次导入:

import { SQRT2, PI } from './cool';

默认符号和非默认符号也可以一起包含在单个导入行中:

import myCoolFunction, { SQRT2, PI } from './cool';

如果要更详细地了解导入和导出,请参阅 JavaScript 参考中的导入导出部分。

变量和常量

较旧的 JavaScript 版本在如何声明变量方面非常草率。从 ES6 开始,和 关键字分别用于声明变量和常量。您可能已经看到用于在旧版本的 JavaScript 中声明变量的关键字。关键字有一些范围划分怪癖,因此最好将其替换为更可预测的 。letconstvarvarlet

要定义变量,只需在前面加上关键字:let

let a;

也可以声明一个变量并同时为其提供一个初始值:

let a = 1;

如果未给出初始值,则为变量分配特殊值 。undefined

常量是一个变量,只有在声明时才能为其赋值:

const c = 3;

console.log(c); // 3
c = 4;  // error

虽然它可能看起来令人困惑,但创建一个常量并为其分配可变对象是完全合法的。例如:

const d = [1, 2, 3];

d.push(4);  // allowed
console.log(d)  // [1, 2, 3, 4]

为什么会这样?因为对常量的要求是它们在声明后没有新的赋值。没有关于改变最初分配的值的要求。

脚本参考文档包含有关允许构造的更多信息

平等与不平等比较

较旧的JavaScript实现在不同类型之间自动转换值方面有非常奇怪的规则。出于这个原因,原始的相等()和不等式()运算符的工作方式可能看起来是错误的,或者至少与你所期望的不同。==!=

为了避免破坏旧代码,这些比较运算符保留了奇怪的行为,但最新版本的 JavaScript 引入了新的比较运算符和 ,以便可以使用更可预测的比较。===!==

通常,所有相等和不等式比较都应使用较新的运算符。例子:

let a = 1;

console.log(a === 1);  // true
console.log(a === '1');  // false
console.log(a !== '1');  // true

鉴于许多其他语言使用 and 运算符进行比较,因此在编写 JavaScript 时无意中使用这些运算符是很常见的。在正确设置的项目(例如您将使用本书构建的项目)中,静态代码分析工具可以检测并警告此错误。==!=

有关更多详细信息,请参阅 JavaScript 参考文档中的严格相等和严格不等式运算符。

字符串插值

很多时候,有必要创建一个包含静态文本和变量混合的字符串。ES6 使用模板文本来实现此目的:

const name = 'susan';
let greeting = `Hello, ${name}!`;  // "Hello, susan!"

模板文本参考文档中有更多示例。

For-For 循环

旧版本的JavaScript只提供奇怪和扭曲的方式来迭代一个元素数组,但幸运的是,ES6为此目的引入了该语句。for ... of

给定一个数组,可以按如下方式构造一个循环循环其元素:

const allTheNames = ['susan', 'john', 'alice'];
for (name of allTheNames) {
  console.log(name);
}

箭头函数

ES6 为函数的定义引入了一种替代语法,该语法除了对变量具有比关键字更一致的行为外,还更加简洁。thisfunction

请考虑以下以传统方式定义的函数:

function mult(x, y) {
  const result = x * y;
  return result;
}

mult(2, 3);  // 6

使用较新的箭头函数语法,可以按如下方式编写函数:

const mult = (x, y) => {
  const result = x * y;
  return result;
};

mult(2, 3);  // 6

从这个角度来看,目前还不是很清楚为什么箭头语法更好,但是可以通过几种方式简化此语法。如果函数具有单个语句而不是两个语句,则可以省略大括号和关键字,并且可以将整个函数写入一行:return

const mult = (x, y) => x * y;

如果函数接受单个参数而不是两个参数,则括号也可以省略:

const square = x => x * x;

square(2);  // 4

将回调函数作为参数传递给另一个函数时,箭头函数语法更方便。请考虑以下示例,该示例使用传统函数和箭头函数定义显示:

longTask(function (result) { console.log(result); });

longTask(result => console.log(result));

有关详细信息,请参阅 Arrow 函数文档。

承诺

promise 是返回给在后台运行的异步操作的调用方的代理对象。调用方可以使用此对象来跟踪后台任务,并在任务完成时从中获取结果。

promise 对象具有 和 方法(以及其他方法),这些方法允许构造具有可靠错误处理的异步操作链。then()catch()

许多内部和第三方 JavaScript 库都返回了承诺。下面是使用该函数发出 HTTP 请求,然后打印响应的状态代码的示例:fetch()

fetch('https://example.com').then(r => console.log(r.status));

这将在后台执行 HTTP 请求。读取操作完成后,将调用作为参数传递给方法的箭头函数,并将响应对象作为参数。then()

承诺可以被链接起来。需要链接的常见情况是,当发出 HTTP 请求时,该请求返回包含一些数据的响应。以下示例显示如何将请求操作链接到从服务器响应读取和分析 JSON 数据的第二个后台操作:

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data));

这仍然是一个单一的陈述,但我已将其分解为多行以提高清晰度。调用完成后,传递给第一个执行的回调函数将使用响应对象作为参数。这个回调函数返回,一个响应对象的方法,也返回一个承诺。第二个调用在第二个承诺完成时调用,接收解析的 JSON 数据作为参数。fetch()then()r.json()then()

要处理错误,可以将该方法添加到链中:catch()

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data))
  .catch(error => console.log(`Error: ${error}`));

有关承诺的其他详细信息,请参阅承诺 API 文档

异步和等待

Promise 是一个很好的改进,有助于简化异步操作的处理,但是必须在长调用序列中链接多个操作仍会生成难以阅读和维护的代码。then()

在 ECMAScript 的 2017 年修订版中,引入了 和 关键字作为处理承诺的替代方式。下面是上一节中的第一个示例:asyncawaitfetch()

fetch('http://example.com/data.json')
  .then(r => r.json())
  .then(data => console.log(data));

使用异步/await 语法,可以按如下方式进行编码:

async function f() {
  const r = await fetch('https://example.com/data.json');
  const data = await r.json();
  console.log(data);
}

使用此语法,可以按顺序给出异步任务,并且生成的代码看起来非常接近同步函数调用。一个限制是关键字只能在用 声明的函数内使用。awaitasync

异步函数中的错误处理可以使用 来实现:try/catch

async function f() {
  try {
    const r = await fetch('https://example.com/data.json');
    const data = await r.json();
    console.log(data);
  }
  catch (error) {
    console.log(`Error: ${error}`);
  }
}

声明为的函数的一个有趣的特性是,它们会自动升级以返回承诺。如果需要,可以使用以下方法将上述函数链接到其他异步任务:asyncf()then()

f().then(() => console.log('done!'));

或者,当然,如果调用函数也是异步的,也可以等待它:

async function g() {
  await f();
  console.log('done!');
}

箭头函数语法也可以与函数一起使用:async

const g = async () => {
  await f();
  console.log('done!');
};

JavaScript 文档的“使用异步和等待使异步编程更简单”部分是了解更多信息的好地方。

展开运算符

operator () 可用于就地展开数组或对象。这允许在处理数组或对象时使用非常简洁的表达式。了解点差操作员的最佳方法是通过一些示例。...

假设您有一个包含一些数字的数组,并且想要找到其中最小的数字。传统的方法可以做到这一点,这需要一个for循环。使用 spread 运算符,您可以利用该函数,该函数采用参数的变量列表:Math.min()

const a = [5, 3, 9, 2, 7];
console.log(Math.min(...a));  // 2

基本思想是表达式扩展 的内容,因此函数接收五个独立的参数,而不是单个数组参数。...aaMath.min()

spread 运算符还可用于通过将另一个数组与新元素混合来创建新数组:

const a = [5, 3, 9, 2, 7];
const b = [10, ...a, 8, 0];  // [10, 5, 3, 9, 2, 7, 8, 0]

它还允许一种简单的方法来执行数组的浅层复制:

const c = [...a];  // [5, 3, 9, 2, 7]

跨页语法也适用于对象:

const d = {name: 'susan'};
const e = {...d, age: 20};  // {name: 'susan', age: 20}
const f = {...d};  // {name: 'susan'}

对象上散布运算符的一个有趣的用法是进行部分更新:

const user = {name: 'susan', age: 20};
const new_user = {...user, age: 21};  // {name: 'susan', age: 21}

在这里,使用最后显示的版本来解决在键具有两个值时发生的冲突。age

有关更多详细信息,请参阅跨页语法参考。

对象属性速记

在与散布运算符相同的联盟中,对象属性速记为对象属性提供了简化的语法。请考虑如何创建以下对象:

const name = 'susan';
const age = 20;
const user = {name: name, age: age};

你看到 和 的重复吗?这些关键字用作属性名称,也用作保存属性值的常量的名称。使用 object 属性速记语法,可以按如下方式创建相同的对象:nameage

const user = {name, age};

如上所述创建的对象使用给定变量或常量的名称作为属性名称,并将值分配给新属性。

速记属性和正则属性也可以组合在一起:

const user = {name, age, active: true};  // {name: 'susan', age: 20, active: true}

解构分配

解构赋值是另一个不错的语法速记,可用于简化数组和对象的赋值。这个想法是,作为赋值操作的一部分,右侧值可以动态分解为其元素。下面是一个数组示例:

const a = ['susan', 20];
let name, age;
[name, age] = a;

赋值左侧的方括号告诉 JavaScript 右侧是一个数组,在将元素分配给变量列表之前必须将其拆开。

如果左侧和右侧的元素数不匹配,会发生什么情况?如果左侧的元素多于右侧,则会为左侧的额外元素分配该值。如果右侧的元素多于左侧的元素,则多余的元素将被丢弃。undefined

在解构赋值和上面讨论的点差运算符之间有一个有趣的组合。请考虑以下示例:

const b = [1, 2, 3, 4, 5];
let c, d, e;
[c, d, ...e] = b;
console.log(c);  // 1
console.log(d);  // 2
console.log(e);  // [3, 4, 5]

解构赋值也可以用于对象:

const user = {name: 'susan', active: true, age: 20};
const {name, age} = user;
console.log(name);  // susan
console.log(age);  // 20

此技术不仅可以应用于直接赋值,还可以应用于函数参数。下面的示例演示了它:

const f = ({ name, age }) => {
  console.log(name);  // susan
  console.log(age);  // 20
};

const user = {name: 'susan', active: true, age: 20};
f(user);

此处,arrow 函数接受对象作为其唯一参数,但该函数仅从输入中获取 and 属性,而不是接受整个对象。与赋值一样,如果对象具有其他属性,则会丢弃这些属性,如果函数声明中的任何命名属性不在对象中,则会为它们分配值。f()nameageundefined

要了解有关使用此功能的更多方法,请参阅 JavaScript 参考的解构分配部分。

在早期版本的JavaScript语言中,包括ES5在内的一个很大的遗漏是,它们是面向对象编程的核心组件。下面您可以看到一个 ES6 样式类的示例:

class User {
  constructor(name, age, active) {  // constructor
    this.name = name;
    this.age = age;
    this.active = active;
  }

  isActive() {  // standard method
    return this.active;
  }

  async read() {  // async method
    const r = await fetch(`https://example.org/user/${this.name}`);
    const data = await r.json();
    return data;
  }
}

要创建类的实例,请使用关键字:new

const user = new User('susan', 20, true);

有关的更多信息,请参阅脚本参考。

断续器

本章中讨论的最后一个现代 JavaScript 功能属于自己的类别,因为它不是任何 ECMAScript 规范的一部分,而且它永远不会成为该规范的一部分。它被称为 JSX,它是脚本 XML 的缩写。其目的是使创建内联结构化内容变得更加容易,主要用于HTML页面。

假设您需要创建一个 HTML 段落元素。使用普通脚本和 DOM API,您可以按如下方式创建此元素:<p>

const paragraph = document.createElement('p');
paragraph.innerText = 'Hello, world!';

你能想象使用普通的JavaScript创建一个更复杂的元素树会是什么样子吗,比如一个完整的表?使用JSX,它变得更容易,并且生成的代码更具可读性:

const paragraph = <p>Hello, world!</p>;

下面是一个更复杂的 HTML 表示例:

const myTable = (
  <table>
    <tr>
      <th>Name</th>
      <th>Age</th>
    </tr>
    <tr>
      <td>Susan</td>
      <td>20</td>
    </tr>
    <tr>
      <td>John</td>
      <td>45</td>
    </tr>
  </table>
);

JSX 语法是 React 应用程序的关键组件。虽然从技术上讲,React 不需要它,但它使HTML内容和模板更容易创建和维护。