手记

JavaScript 从 0 到 1 入门手册(2020版)

前言

在学习 JavaScript 的过程中,我们通常会把 JavaScript 分为以下三个部分

  1. JavaScript核心语言(The Core (ECMAScript))
  2. 文档对象模型(The Document Object Model (DOM))
  3. 浏览器对象模型(The Browser Object Model (BOM))

本手册将把重点放到 JavaScript 语言本身身上,也就是第一部分 JavaScript 核心语言(The Core (ECMAScript)

如果你把 JavaScript 选为你的第一门语言,那么希望本手册能够帮助到你尽快掌握它。

如果你已经在日常开发中使用 JavaScript 了,那么本手册的内容也是一个很好的复习。

现在,让我们从 JavaScript 的历史开始。

了解历史

在正式开始学习前,我们先来简短地看一下 JavaScript 的历史,这有助于第一次接触 JavaScript 的人更好地学习。

诞生

JavaScript 创建于 1995 年。由当时 Netscape (网景) 公司的 Brendan Eich (布兰登·艾克) 负责开发,最初命名为 Mocha,后来改名为 LiveScript,最后才重命名为 JavaScript

在创建的初期,JavaScript 并没有对应的标准(没有统一的语法或功能),这造成了浏览器脚本编写的困难,所以当时业界都希望可以把 JavaScript 语言标准化

标准化

1997 年,JavaScript 1.1 版本被提交到了 ECMA(欧洲计算机制造商协会)ECMA 把它分配给了 TC39 技术委员会,希望可以制定出一个标准、通用、跨平台且和各大浏览器厂商都无关的标准。

TC39 技术委员会在当时包括了 NetscapeSunMicrosoftBorland 等占据市场主流的技术公司,几个月的技术会议后,最终发布了 ECMA-262

ECMA-262 这份标准定义了一种新的脚本语言,名称就是我们现在听到的 ECMAScript,至于为什么叫 ECMAScript 而不是就叫 JavaScript,这有可能是因为当时一些相关的法律或者品牌的原因所造成的。

版本名称的变化

JavaScript 其实是 ECMAScript 标准的实现,这就是为什么您会听到有关 ES6ES2015ES2016ES2017ES2018ES2019ES2020 等的原因,ES 指的就是 ECMAScript

如果你看到我上面罗列的一些版本名称,你会奇怪有一个 ES6,而其他都是 ES + 年份,这其实是因为这个更改的时间有点晚。

当时 ES5 是 2009 年时发布 ECMAScript 规范的名称,这导致 ES5 的下一个版本被人们称为 ES6,而且官方决定宣布使用 ES2015 而不是 ES6 的时间点其实很晚,这也就导致了目前在社区会有 ES6ES7ES8ES9ES10 这些版本名称的出现,但是其实 ES + 年份才是目前的官方名称。

其他

除了 JavaScript 外,其实也有其他语言也实现了 ECMAScript,比如 ActionScript,它是 Flash 的脚本语言,但是目前已经逐渐消失了。

现在,支持 ECMAScript 规范的语言就是 JavaScript

其中,ES5 从 2009 年到现在,已经过去超过 10 年,虽然 ES5 在 JavaScript 的历史中是一个非常重要的版本,但是 ES5 的很多知识已经不值得再投入过多时间。

现在,我们应该转向 ES2015(ES6) 或之后的版本进行学习。

语法概述

大小写

JavaScript 是区分大小写的,不管是变量、函数名还是运算符都区分大小写,比如变量名 Apple 和 apple 就代表了两个变量。

标识符的合法性

我们上面提到的变量 apple,其实就是程序中的标识符

标识符是用于描述程序中变量函数属性参数等的名称。

在 JavaScript 中,一个合法的标识符规则如下:

  • 第一个字符必须是字母下划线( _ )美元符号( $ )
  • 其他所有字符可以是字母下划线( _ )美元符号( $ )数字
  • 不能是 JavaScript 中的关键字保留字

另外,对于标识符,有三点需要提到:

  1. 标识符如果包含 两个或两个以上 的单词时,那么应该采用 驼峰式 的写法,第一个单词的首字母小写,后面的单词首字母大写,比如 myApple。
  2. 美元符号通常在引用 DOM 元素时被使用,又或者被一些常用的库所使用(比如 jQuery),所以平时我们命名变量也很少使用美元符号( $ )
  3. 我们很少会去背关键字保留字,一般都是学习中或使用中慢慢熟悉。

注释

JavaScript 采用 C语言 风格的注释,即使用 //单行注释,和使用 /* */多行注释

如:

// 单行注释

/*
多
行注释
*/

分号

分号(;) 用于结束一条语句,而一些足够聪明的解释器可以识别一条语句什么时候结束。

这就导致了目前的一个争议,一些开发人员建议始终使用分号,而另一些开发人员认为不需要使用分号

这里的重点是,保持统一的做法

如果使用分号就都使用,不要在一个项目中,一个地方使用分号,另一个地方不使用。

以下两条语句的区别只在于代码风格的不同:

const hello = 1;
let world = 2

风格可以讨论,但不需要一个定论。

值和类型

JavaScript 有很多种类型,但是目前还没到展开的时候,我们现在需要了解的是每一个都有对应的类型

比如 100 是一个,而数字是这个类型。又比如 apple 是一个,而字符串是这个类型

通常,我们会说,字符串 apple,数字 100。

而当我们需要使用这些的时候,我们需要把它们存储到变量中,而每个变量都有一个变量名(也就是标识符)来标识它,这样我们就可以通过变量名来找到我们要使用的了。

变量

首先,变量是什么?

对于初学者来说,一个易于理解的例子就是,变量是程序中的一个盒子,并且盒子上贴有唯一的标签(变量名),而盒子里面的内容,就是我们的

另外,可以想象盒子的大小或者形状则是我们的类型

我们将使用两个关键字去声明变量,一个是 const(常量),另一个是 let(变量)

const foo = 1
let bar = 2

const

const(常量) 声明了该变量不能为这个变量重新分配一个值了。

例如,以下操作将会报错:

const foo = 1
foo = 2

JavaScript中的常量和一些编程语言中的常量是有区别的。

对于 const,我们并没有说不能改变它的值,而是说不能为这个变量重新赋值。

那么,如果我们使用 const 声明一个对象,其实是可以修改这个对象的属性的。

这一点,我们将在学习对象中讨论。

关于常量,我们还需要提到关于命名。

现实中,对于常量的命名,我们有一个常规做法就是都大写,比如上面的例子,我们将命名为 FOO

但是也有另外一种情况,就是我们的常量的值并非是提前知道的,而是需要在执行期间才能获得,那么,此时的常量的命名仍然使用驼峰式

以下 const 变量名命名的例子:

const APPLE_COLOR_RED = "#F00"
const codeExcuTime = /* 值将来自代码执行后 */

let

let 声明的变量可以重新分配一个值。
例如,以下操作将会成功:

let bar = 2
bar = 3

var

var 是 ES6 之前用于声明变量关键字

下面是一个例子

var foo = 2

var 类似我们的 let,这里不会进入过多的讨论,现实中,我们应该避免使用 var

建议

总的来说,我们通过声明变量来存放我们的数据。

对于现在,我们应该只使用两个关键字来声明变量,letconst

其中的不同是,如果是使用 const 声明变量,那就表示我们不希望这个变量的值被再次赋值。

注意,JavaScript 中,const 虽然表示常量,但并没有表示不能修改它的值。

我们也提到的另外一个关键字 var,但我认为应该在你需要的时候再花时间去了解它。

项目中,我们应该避免使用 var,更多考虑使用的是 const,然后才是 let

类型

从一般的编程概念来看,变量的类型定义了可以存放到这个变量中的,以及可以对这个值所进行的操作

比如一个变量存放的值是数字类型,那么这个变量可以执行加减乘除操作。

通常,我们会把类型划分为两类:

  • Primitive Types(原始类型)
  • Object Types (对象类型)

原始类型

JavaScript 中,原始类型包括了 NumberBigIntStringBooleanSymbol

另外,我们把 NullUndefined 这两个特殊的类型也划分到原始类型中,所以总共有 7原始类型

对象类型

对象类型,也就是 Object 类型

可以说 JavaScript 中,除去原始类型就是对象类型了。
对象类型涉及到 properties(属性)methods(方法),我们将会在对象的知识点中讨论。

在各大经典的 JavaScript 教程中都使用了原始类型对象类型来对 JavaScript 中的类型进行划分。

但是在规范(ECAMScript2020)中并没有这种划分。只是直接列出了 8 种类型,也就是我们提到的 UndefinedNullBooleanStringSymbolNumberBigIntObject

其中 SymbolBigInt 算是后加进来的,所以在一些旧的教程中,你可能看到的是 6 种,而不是 8 种。

表达式

表达式概述

表达式总是和语句产生关联,这也在社区中造成了一些争议,但从掌握一门编程语言的过程来看,我认为区分表达式语句还是有必要的,这对我们学习和理解函数式编程也有帮助。

表达式和语句的区别

语句其实是由关键字组成的一条命令,用于告诉 JavaScript 做什么,比如我们常会说的导入某某库,这就需要我们写语句来告诉 JavaScript了。

以下是一个语句的另外一个例子,用于告诉 JavaScript,我们声明了一个变量,并把一个值存储到该变量中:

let foo = 101

表达式可以看做就是值,以下就是一些表达式

101
1.38
'apple'
true
false

我们可以看到,表达式其实是表达一个,这就导致了表达式通常被放到等号的右边。

语句通常都会涉及到关键字,比如循环语句,用于告诉 JavaScript 这里需要重复执行。

在我们后面接触的语句越来越多的时候,就可以更好地理解了。

表达式的分类

通过表达式,我们会得到一个,这里如果细分,又可以分为算术表达式,这将得到一个数字,比如:

1 + 2
i++
i * 100

又或者字符串表达式逻辑表达式,比如:

// 结果是 hello world!
’hello' + ' ' + 'world!'
// 结果是 true 或者 false
isCar && isHouse

除此之外,还可以是我们后面学到的函数对象数组等,后面的章节将会介绍它们。

运算符

运算符的其实是 Operator 的翻译。

前面我们提到,类型定义了可以对值所进行的操作

所以,通常的翻译是操作符,而运算符,我觉得更多是数学上的概念,可能会更通俗一些。

但某些操作使用操作符可能会更好理解,所以,有时候也会使用操作符

赋值运算符

我们已经见过不少运算符了,第一个要正式介绍的运算符我们也已经见过,就是等号( = )

等号其实是赋值运算符,用于分配

另一种说法是初始化

以下就是一个初始化变量的例子:

// 把 score 初始化为 100
let score = 100

算术运算符

最常见的算术运算符当然就是 加( + )减( - )乘( * )除( / ) 了。

除此之外,还有取余(%)求幂()** 。

以下是 例子:

let foo = 1 + 2
foo = 1 - 2
foo = 1 * 2
foo = 1 / 2
foo = 2 % 3
foo = 1 ** 2

Infinity 和 NaN

InfinityNaN 其实 JavaScript 全局对象中的属性属性的值就是他们本身。

我们还没有聊得到对象,但是这里由于算术表达式会导致这两个值出现,所以我们有必要了解一下。

Infinity

除法中,如果除以零,JavaScript 给出的结果是Infinity(正无穷大),或者是 -Infinity(负无穷大),而不是报错。

let foo = 1 / 0 // Infinity
foo = -1 / 0 // -Infinity

另外,Infinity 和数学中的无穷大概念很类似,比如任何正数乘以 Infinity 会得到 正Infinity(负数则得负 Infinity,任何正数值除以 Infinity,则得到正 0。

NaN

NaN(Not-A-Number) 表示不是一个数字。

我们都知道 0 不能做除数,那么如果这么做,JavaScript 会告诉你得到的不是一个数字,也就是 NaN

如下是一个例子:

let foo = 1 % 0 // NaN
foo = -1 % 0 // NaN

通常 NaN 的出现,其实就和它的含义一样,通过计算,得到了一个不能表示为数字的值。

比如操作符中两个变量的类型不同,又或者其中一个变量的值已经为 NaN 了.
如:

'a' / 1 // NaN
1 + ('a' / 1) // NaN

对于运算中何时会出现 Infinity,何时会出现 NaN,在 ECMA 的规范中罗列了很多情况,刚开始学习的时候我们并没有必要每一条都找出来,只需要知道这两个值得含义就好了。

下图是 ECMA2020 中关于仅关于除法操作出现的情况,大家感受一下:

运算符的优先级

我们都知道在数学中,加减乘除运算是有优先级的,通常口诀就是先乘除,后加减

在 JavaScript 中也是一样的,运算符优先级决定了运算执行的先后顺序

比如:

1 + 2 * 3 // 1 + 6 结果为 7

所有操作符都有优先级,有人汇总成了一个表格,但是我们并不需要去背这个。

因为我们有一个处理优先级的超强办法,就是使用小括号(或者说是圆括号)

下面是一个例子:

(1 + 2) * 3 // 3 * 3 结果为 9

小括号优先级最高的,并且本身没有关联性

关联性指的是,如果大家的优先级相同,那么我们应该怎么执行。

一般来说,关联性分为左关联(从左到右执行)右关联(从右到左执行)

比如我们的加减乘除就是左关联的,那么 1 + 2 + 3,会优先考虑计算 1 加 2,然后再加 3 了。

右关联的一个例子就是我们的赋值操作符

在一条赋值语句中,JavaScript 会把等号右边的表达式算出来后再赋值给等号左边的变量中:

let foo = 1 + 2 * 3 // 把7算出来后,再存储到 foo 变量中

比较运算符

第三个要了解的运算符是比较运算符

比较过后,我们会得到一个布尔类型的值,总共就两个,分别是 truefalse

常见的比较运算符如下:

  • < (小于)
  • <= (小于或等于)
  • >(大于)
  • >=(大于或等于)
  • === (是否相等)
  • !== (是否不相等)

下面是例子:

let foo = 1
foo >= 1 // true

关于相等不相等,其实还有另外的运算符,==!=。对于初学者来说,我们应该选择使用 === 来进行检查是否相等,使用 !== 来检查是否不相等

这也是现实编程中的常规选择。

有了比较运算符之后,我们就可以讨论开始条件语句了。

条件语句

通常,代码会从上到下一行接一行地被执行。

但是有时候,我们需要根据条件来选择执行不同的代码。

为此,我们使用 if 语句。

if 语句实在是太常用了,以至于几乎所有的编程语言都会提供,所以当你掌握一种编程语言的 if 语句后,其他语言的完全不在话下。

三种 if 语句

一般来说,和其他编程语言一样,JavaScript 也提供了 if 语句的三种语法:

// 如果条件为真(true),则执行大括号里面的语句。
if (condition) {
  statements
}
/**
 * 如果条件为真(true),则执行大括号里面的语句。
 * 否则执行else语句块的内容。
 */
if (condition) {
  statements
}
/**
 * 如果条件1为真(true),则执行大括号里面的语句。
 * 否则通过 else if 语句判断条件2,为真(true)则执行大括号里面的语句。
 * 如果前面的if和elseif(一个或多个)都不为真(true),则执行else语句块的内容。
 */
 if (condition_1) {
   statements
 } else if (condition_2) {
   statements
 } else {
   statements
 }

如果你有学过其他编程语言的 if语句 的话,那么上面就是 JavaScript 中的 if语句 的用法,复习一下之后,你就可以进入下一节的内容了。

我们来一个个看,首先是一个第一个简单的例子:

// 永远被执行
if (true) {
  const score = 100;
}
// 永远不被执行I 
if (false) {
  const socre = 0;
}

接着我们可以为 if语句 提供 else,作为当 if 为假的时候的选择:

// 如果是真,score赋值为100,否则赋值为0
if (true) {
  const score = 100;
} else {
  const score = 0;
}

最后,我们的 else 其实也是可以添加语句的,所以,我们可以嵌套一个 if/esle 语句,这样,我们就可以对多个条件进行判断:

if (score === 0) {
  // ......
} else if ( foo === true) {
  // ......
} else {
  // ......
}

注意,一个 if 语句中,else if 可以写多个,但 ifelse 只能写一个。

语句块和作用域

下面是 if语句 的一个简单例子:

if (true) {
  // ......
}

现在,我们来关注花括号花括号代表了语句块,也就是我们说的 block

语句块表面上看,就是可以让我们把一些语句放在一起。

当然,一条语句也可以放到花括号中,变成语句块

但是,使用花括号对我们的语句进行分组并不是最需要理解的概念。

最重要的是,语句块定义了一个新的范围,我们称之为作用域(scope)

作用域 通俗的理解就是代码的可见范围

if (true) {
    let score = 1
}
/**
  * 当我们直接使用score的时候,将报错
  * 报错:ReferenceError: score is not defined
  * console.log() 用于向控制台打印内容。
  */
console.log(score)

上面的例子中,if语句块 中定义了的变量 score。

但在语句块外面想使用时,却告诉我们这个变量没有定义的。

因为语句块划分了可见范围,语句块里面和外面,就好像两个世界一样。

示例中 if 语句的语句块所形成的作用域,我们也称为块级作用域,而在 ES2015,也就是 ES6 之前,并不存在块级作用域

ES5 中的 var 没有块级作用域,是我们放弃使用 var 的重要原因。

数组

通过前面类型的学习,我们知道目前 JavaScript 中只有 8 种数据类型,其中并没有包括数组。

也就是说数组本身不是一种类型。

数组的作用是可以保存多个值,更官方的说法是,数组是元素集合

元素,简单来说就是值,反向地说,元素是组成了某个东西的一些东西。

之前我们对 8 种类型进行了划分,提到除了原始类型之外,就都是对象类型

那么,数组其实就是对象了。

初始化

首先我们看看如何初始化数组一个空数组

const foo = []
const bar = Array()

上面是两种初始化空数组的方法。

第一种是直接使用了中括号语法。

第二种其实是使用了Array函数

我们还没学到函数,目前先知道这使用了函数就可以了。

我们再看看如何初始化一个有元素的数组:

const foo = [1, 2, 3]
const bar = Array().of(1, 2, 3)

数组其实可以包含不同类型的值,甚至可以包括另外一个数组:

const foo = [1, ’apple', [100]]

现实中,除非有很好的理由,否则一个数组应该只放一种类型的元素。

至于多维数组,等需要的时候再去了解吧。

访问元素

要访问数组中元素,我们需要通过数组中的索引

const foo = [1, 2, 3] 
foo[0] // 取得1

索引就是数组中元素的唯一编号,所以通过索引就找到对应的元素了。

我们可以看出,数组的索引其实是从 0 开始,编程的世界大都如此。

数组的操作

因为数组不在 7 个原始类型之中,所以我们知道数组其实是对象,也就是说数组本身应该有一些行为(函数)。

如果完全没有对象或者函数的概念,那么可以先学完相关的知识后再回到这里。

函数很多,希望本手册随着更新会越来越全。

快速填充数组

使用 fill 可以为我快速初始化一个数组:

// 初始化一个长度为3的数组,里面的元素都设置为100
const foo = Array(3).fill(100)

获取数组长度

要知道数组的长度,我们可以使用 length 属性:

const foo = [0, 1, 3]
foo.length // 3

增添元素

我们可以使用 push() 函数,该方法在数组的尾部添加元素:

let foo = [1, 2]
foo.push(3) // 1, 2, 3

而使用unshift()方法则在头部添加元素:

let foo = [1, 2]
foo.unshift(3) // 3, 1, 2

取出元素

和添加类似,我们可以使用 pop()shift() 函数。

pop() 用于取出数组尾部的元素:

let foo = [1, 2, 3]
foo.pop() // foo变成1, 2

shift() 用于取出数组头部的元素:

let foo = [1, 2, 3]
foo.shift() // foo变成2, 3

删除元素

删除元素,我们可以使用 delete。

let foo = [1, 2, 3]
delete foo[1] // 删除索引为1的元素
console.log(foo)  // [1, ,3]
console.log(foo.length) // 3

注意,使用delete删除数组中的元素,只是移除了元素的值。

元素原本的位置仍被占据着,数组的长度不会变。

拼接数组

拼接数组,我们可以使用 concat() 函数:

const foo = [1, 2]
const bar = [3, 4]
const baz = foo.concat(bar) // [1, 2, 3, 4]

除此之外,使用 扩展运算符 也是可以的:

const foo = [1, 2]
const bar = [3, 4]
const baz = [...a, ...b] // [1, 2, 3, 4]

查找元素

查找元素,我们使用 find() 函数:

let foo = [
  {id: 1, score: 80},
  {id: 2, score: 90},
  {id: 3, score: 100}
];

let bar = foo.find(item => item.id == 1);

console.log(bar.score); // 80
console.log(bar); // { id: 1, score: 80 }

相比起来,find 的用法稍微复杂,但是对于存储对象的数组来说,非常有用。

在上面的例子中,我们通过传递一个只有一个参数的函数 item => item.id == 1 来使用 find,这也是现实中使用较多的用法,其他参数的一般很少使用。

另外就是如果想找出数组中元素的索引,我们只需要把 find 改为 findIndex 就可以了。

字符串

字符串也就是 String,在编程语言中通常占据重要地位。

多个字符就组成了字符串,更官方准确的说法就是字符序列

声明字符串

在 JavaScript 中,定义字符串,使用单引号双引号都可以:

'I am string.'
"I am string too."

一般来说,一个项目除了 JavaScript 文件外,还有 HTML 文件,所以更多建议 JavaScript 中使用单引号,而在 HTML 中使用双引号

字符串的操作

既然字符串是一个字符序列,那么自然就会有相关操作了。

首先看看如何初始化一个字符串变量:

const foo = 'dog'

获取字符串长度

使用 length 属性查看字符串的长度:

const foo = 'dog'
console.log(foo.length) // 3

字符串可以看做是一个字符数组,所以注意,字符串的索引也是从 0 开始的。

字符串的连接

连接两个字符串,我们可以直接使用 “+” 号:

const foo = 'Hello' + ' World!'
console.log(foo) // Hello World!

变量也是可以的:

const foo = 'Hello'
const bar = foo + ' World!'
console.log(bar) // Hello World!

定义多行字符串

有时候,我们希望做一些模拟数据,需要定义多行字符串,我们可以使用反引号:

const string = `Hello
this 
 is string
World`

循环

循环是所有编程语言中必须要有的语句之一。

通过循环,我们可以重复执行一段代码。

通常来说,whilefor 是主要的内容。

JavaScript 中有很多种循环的方法,我们这里介绍三种:

  • while
  • for
  • for…of

while

while 应该是最简单的一种了:

while (condition) {
  statement
}

其中 condition条件表达式,也就是它需要一个布尔值,真(true)或假(false)。

如果是真,就执行语句块里的语句,每执行完一次语句块,就重新执行一次条件表达式,看一下值是真还是假。

如果是真,就继续执行语句块,如果是假,循环就停止了。

使用 while 语句,重要的是需要添加能控制条件表达式在某次循环得到一个假的值,不然循环一直下去,无法停止的循环,我们称为死循环

除此之外,对于第一个介绍的循环语句,我还想结束一些其他的概念,以便更好地学习后面的 for 循环。

下面是另外一个例子:

const foo = ['a', 'b', 'c']
let i = 0
while (i < foo.length) {
  console.log(foo[i]) // 将打印 a, b, c
  console.log(i) // 将打印 0, 1, 2
  i = i + 1 // 每次循环都需要递增 1
}

首先,while 语句后面的小括号的内容是条件表达式

条件判断为真的时候就执行后面语句块的内容。

也就是说当通过计算, i < foo.lenght 得到的值是 true 的时候,后面的语句块就会被执行。

在循环语句中,语句块也叫 循环体

循环语句中还有一个迭代的概念,通常,循环体 被执行一次,我们也叫 一次迭代,上面的例子就是 三次迭代 了。

如果例子中没有 i = i + 1 这条语句的话,那么理论上说,循环体会一直被执行。

另外,只要最终可以被 JavaScript 转化为一个布尔值的表达式,就都可以放到循环条件中,而不仅仅是大小运算符比较的条件表达式。

for

当使用 for 关键字的时候,我们一般会告诉 for 三件事情:

  • 初始化变量是什么
  • 循环条件是什么
  • 每次循环后需要执行什么

下面是一个例子

for (let i = 0; i < 3; i++) { // 结果为 0、1、2
  console.log(i);
}

上面的例子中,初始化变量是 i,循环条件是 i < 3,每次循环过后执行 i++

++ 是自增运算符,简单来说就是让自己加1。

由于每次循环后,i 都会加1,那么,i 将会在某次循环过后,它的值将不是小于3的了。

此时,循环终止。

如果你是第一次接触 for 循环语句,那么还需要知道的是,三个表达式都是可选,极端的情况是都不写(但是分号不可省略)。

另外,在不同的编程语言,或者不同的教程中,对 for 语句中的三个表达式有不一样的称呼。

比如 ECMAScript2020 文档,因为是标准,所以简单明了,对这三个表达式都称呼为表达式,然后使用 opt 代表都是可选的,大概像下面这样:

for(Expression(opt); Expression(opt); Expression(opt) {
  Statement
}

而在 MDN 的文档中,使用的是 initializationconditionfinal-expression,然后使用中括号代表都是可选的,大概像下面这样:

for ([initialization]; [condition]; [final-expression]) {
  statement
}
   

其实除了标准是简单粗暴地称为表达式外,其他教程几乎都会对 for 循环里的三个表达式采用一个不同的称呼。

这都是为了让读者更好地明白 for 语句是怎么用的。

我们这里把三个表达式,从左到右编号,就是表达式1,表达式2,表达式3:

for (表达式1; 表达式2; 表达式3)
   statement

我们要知道 1,2,3 各自什么时候执行。

其中,记住表达式1只执行一次,然后执行表达式2。

如果表达式2判断为真,那么进入循环体,循环体执行完后,执行表达式3。

这里,其实就是不断循环了,表达式2开始,然后循环体,然后表达式3,之后回到表达式2,然后循环体,然后表达式3,之后回到表达式2,如此循环执行。

直到有一次,表达式2判断为假了,整个循环也就结束了。

for…of

for…ofES2105,也就是 ES6 中才出现,当我们想检查对象的属性时,使用它更加方便,特别是有 key-value 这种数据的时候(比如属性用作“键”),需要检查其中的任何键是否为某值的情况时,还是推荐用for … in。

下面是一个简单的例子:

const hi = ['h', 'i']

for (const value of hi) {
  console.log(value) // hi
}

上面的例子中,我们只是遍历看看,并不改动,所以使用了 const 来声明语句块的变量。

接下来我们,将会进入函数的学习。

函数

终于来到函数了,可以说,函数是 JavaScript 中最重要同时也最有意思的部分。

那么,什么是函数?

在大部分编程语言中,一个函数通常就是一个可以被重复调用的语句块

当然,在 JavaScript 中会有不一样的概念。

JavaScript 中,函数其实是对象,如果你有对象的概念,那么现在就可以知道,函数的名称其实是指向函数对象的一个指针。

同时,函数自己是一个对象,这说明函数本身具有对应的属性和方法。

ok,对函数的解释到这里就可以了。

如果你一头雾水,那么没关系。

因为,对于初学者来说,学习函数就从函数是一个可以被重复调用的语句块 这个概念开始就很好。

让我们从如何声明和调用一个简单的函数开始。

函数的声明和调用

让我们看一下,我们如何声明一个函数:

function sayHello() {
    console.log('Hello!')
}

其中,function 是关键字,用于声明函数,接着是函数名 sayHello

函数名后面是圆括号,圆括号里的内容,我们称呼为形式参数(通常简称形参)。

形参如果出现多个多个,我们使用逗号分隔。

在有多个形参的时候,我们称为形参列表

最后的花括号,我们称之为函数体

目前,我们知道一个函数包括了以下4个内容:

  1. function关键字
  2. 函数名
  3. 形参列表
  4. 函数体

接下来,我们来看另外一个函数:

function sum(num1, num2) {
    return num1 + num2;
}

上面的函数所描述的功能是两数相加。

我们会发现其中多出了一个关键字 return

return 的作用是返回一个值(简称返回值)。

在 JavaScript 中,每个函数其实都会返回一个值,默认情况下,返回 undefined,及时没有写 return语句也是一样。

上面的例子中,我们使用 return 语句来明确我们的返回值是两个形参之和。

我们目前只是声明了一个函数。

如果不调用它们,那么它们并不会自己执行。

要调用一个函数很简单,只需要使用函数名和告诉它形参是什么就可以了。

下面是对上面两个函数进行调用的例子:

sayHello() // Hello!
console.log(sum(1, 2)) // 调用,并把函数返回的结果打印出来

函数表达式

让我们再看一下实现两数相加的函数:

function sum(num1, num2) {
    return num1 + num2;
}

上面的函数其实等同于我们使用一个函数表达式,就像下面这样:

let sum = function(num1, num2) {
    return num1 + num2;
}

如果你忘了什么是表达式,那么建议你回看前面关于表达式的内容。

现在,我们可以像之前那样调用这个函数:

console.log(sum(1, 2)) // 调用,并把函数返回的结果打印出来

在 JavaScript 中,function关键字函数名形参列表函数体这些就构成了我们的函数。

这种函数通常也被我们称呼为普通函数,或者常规函数。

箭头函数

箭头函数在ES6中出现,目前已经变得非常常用了。

有时候,箭头函数和我们上面刚提到的函数表达式很类似。

下面是一个比较,大家感受一下:

// 函数表达式
let sum1 = function(num1, num2) {
    return num1 + num2;
}
// 箭头函数
let sum2 = (num1, num2) => {
    return num1 + num2
}
console.log(sum1(1, 2)) // 打印 3
console.log(sum2(1, 2)) // 打印 3

从外表看,箭头函数会更简单,它不仅省略了 function 关键字,还省略了函数名

比如普通函数:

function sayHello() {
  console.log('Hello!')
}

箭头函数是这样:

() => {
  console.log('Hello!')
}

当我们想像普通函数一样调用箭头函数的时候,我们会发现,由于省略了函数名,我们没办法调用。

所以,我们需要将它赋值给一个变量。

let sayHello = () => {
    console.log('Hello!')
}

这样,我们就可以通过变量名来调用这个箭头函数了:

sayHello()

如果我们只有一条语句,我甚至可以省略花括号:

let sayHello = () => console.log('Hello!')
sayHello() // 将打印 Hello!

可以看到,对于只有一行代码的函数来说,箭头函数展示了它的便捷性。

当然,如果函数体内有多行语句,我们还是要显示地使用花括号和 return 语句。

让我们看看两数之和的箭头函数是怎么样的:

let sum = (num1, num2) => num1 + num2
console.log(sum(1, 2)) // 打印 3

我们可以看到,箭头函数允许我们省略 return 关键字。

如果我们只有一个参数,我们还可以省略圆括号:

const getClassName = className => console.log(className)
getClassName('三年二班') // 打印 三年二班

总的来说,箭头函数的要点是:

  1. 使用函数表达式的方式,但是省略 function 关键字和函数名
  2. 形参列表和函数体之间使用 =>
  3. 当形参列表只有一个参数的时候,我们可以省略圆括号
  4. 当函数体内只有一条语句的时候,我们可以省略花括号,或者 return 关键字。

更多的用法,我们可以在需要的时候再了解。

刚开始接触箭头函数的时候并不容易,但是多尝试几次,习惯之后就会好很多了。

对象

对象是我们的 8 八大类型之一,在 JavaScript 之中,如果不是原始类型,那么就是对象类型了。

对象的创建

我们先来看看如何创建一个对象:

const foo = {

}

上面创建对象的方式是目前 JavaScript 里面最好的特性之一。

这种方式我们称呼为字面量方式。

直接使用花括号,就好像我们使用单引号双引号声明一个字符串一样。

只不过,花括号声明的是对象,而单引号双引号声明的是字符串

除了花括号外,还有以下两种方式:

一种是使用对象的构造函数,语法是 new Object

const foo = new Object()

其中,new 是我们的关键字。

new 可以用于调用对象的构造函数,并以此来初始化对象。

第三种是使用 Object.create()

const foo = Object.create()

尽管有多种创建对象的方法,但是现实中,我们都倾向于使用字面量的方式来创建对象,它更加地可视化,以及使用了更少的代码。

所以,请使用字面量的方式来创建对象。

对象的属性

对象里面通常有属性方法

我们先来看看如何声明对象的属性

下面我们以字面量的方式创建一个对象

let person = {
    name: "Jack",
    "age": 18,
    2: true,
    "likes dogs": true,
}

上面的例子,演示了对象中属性的写法。

首先是属性名,后面跟上冒号,之后是属性的值。

多个属性之间使用逗号分隔。

最后一个属性可以没有逗号,因为在一些老的浏览器中,有逗号会报错。

但是现代的浏览器都支持最后一个属性可以写逗号,所以现在建议最后的属性也加逗号,方便他人也方便自己。

另外,属性名会被自动转换为字符串,所以可以加双引号也可以不加。

但是如果属性名是多个单词的就必须要加上双引号了。

对象的方法

对象中的方法就是我们之前学习的函数,只不过和对象联系起来的时候,我们把函数称呼为方法

下面是一个例子:

let person = {
    name: "Jack",
    age: 18,
    sayHello: function() {
      console.log('Hello!')
    },
}

挺熟悉的是吗,我们也是写了一个属性,只不过属性值是一个没有函数名的函数而已。

我们为 sayHello 属性分配了一个函数,这种情况下,我们就把函数称呼为方法

还记得,我们学过箭头函数吗?

箭头函数也是可以的:

let person = {
    name: "Hello",
    age: 100,
    sayHello: () => console.log('Hello!'),
}

访问对象的属性和方法

我们为对象设置了属性和方法后,怎么调用呢?

和大多数面向对象语言一样,JavaScript 也是使用点操作符来访问对象的属性和方法。

另外,JavaScript 还提供了方括号来访问属性和方法。

下面是一些例子:

let person = {
    name: "Jack",
    "age": 18,
    2: true,
    "likes dogs": true,
    sayHello: () => console.log('Hello!'),
}
console.log(person.name) // Jack
console.log(person.age) // 18
console.log(person[2]) // true
console.log(person["likes dogs"]) // true
person.sayHello()

一般而言,使用点操作符和使用中括号在功能上讲,并没有什么区别。

但是有时候,我们必须使用中括号,如上的 2likes dogs

因为点操作无法使用,原因是点操作符的操作对象需要符合我们标识符的命名规则。

如果大家忘记了合法标识符的那些规则的话,建议回看前面的内容了。

重点是第一个字符必须是字母美元符号($)短下划线(_)

有了点操作符和中括号的概念后,我们可以介绍使用 new 关键字来创建对象了:

let person = new Object();
person.name = "Jack"
person.age = 18
person[2] = true
person["like dogs"] = true

这里,我们使用了点操作符来设置我们的属性,但是像数字和多词属性名就没办法使用点操作符了。我们需要使用中括号。

再次说明,我们更喜欢使用字面量的方式来创建对象。

对象中的箭头函数和普通函数

注意,我们示例中虽然使用了箭头函数作为对象的方法,但有时候我们必须选择普通函数来作为对象的方法。

这是因为在对象中,箭头函数和对象之间根本没有绑定,这导致在箭头函数中的 this 关键字是无效的。

下面是一个例子:

let person = {
    name: "Jack",
    sayHello: function() {
        console.log(`Hello, ${this.name}`)
    },
    anotherHello: () => {
        console.log(`Hello, ${this.name}`)
    }
}
person.sayHello() // Hello, Jack
person.anotherHello() // Hello, undefined

这是箭头函数和普通函数之间的一个区别。

如果我们理解了面向对象,那么,我们很难想象只有对象,而没有类的世界。

因为这样的世界里所有的对象都无迹可寻。

那么类到底是什么呢?

在面向对象的世界里,对于类的解释,流传着这么一句话:类是对象的蓝图。

也就是说,只要我们规划好了类,我们就可以根据类创建出无数个我们想要的对象。

我们也可以把类理解为,一种自定义类型。

我们自己定义了一种类型,然后创建很多相同类型的对象供我们使用。

类的定义、属性和方法

那么如何定义一个类呢?

和大多数面向编程语言一样,在 JavaScript 中我们也是使用 class 关键字和花括号来定义类。

下面是一个例子:

class Person {}

我们虽然定义了一个类,但是,在 JavaScript 中,类其实只是一种特殊的函数。

所以,和函数类似,除了函数表达式外,我们还有类表达式。

下面是一个例子:

const Person = class {}

如果你观察,你会发现 的名称,我们使用大写字母开头。

如有两个或两个以上的单词,仍然使用驼峰式,也就是每个单词的首字母仍然需要大写。

下面我们来定义一个包含属性的类:

class Person {
  name
}

接着我们可以通过 new 关键字初始化一个该类的对象:

const jack = new Person()

和我们之前介绍的对象类似,我们可以使用点操作符来访问其属性:

jack.name = 'Jack'

我们是可以直接在类中初始化属性的:

class Person {
  name = 'Jack'
}

和对象类似,类中除了可以定义属性外,还可以定义方法:

class Person {
  name = 'Jack'
  
  sayHello() {
  	console.log('Hello!')
  }
}

构造方法和 this

类中除了普通的方法外,还有几种特殊的方法,其中一个就是构造方法

构造方法通常用于在我们构造一个对象的同时初始化属性。

构造方法也叫做构造器,在 javascript 中通过使用 constructor() 创建:

class Person {
    constructor(name) {
        this.name = name
    }
    sayHello() {
        console.log(this.name)
    }
}

上面的例子中,我们使用 constructor() 来初始化属性,除了使用了形参列表外,我们还是用了另外一个关键字 this

对于 this,我们可以认为,谁调用,this 就是谁。

比如 我们创建了 jack 对象,然后 jack 对象调用了 sayHello() 方法,那么 this 指的就是 jack 对象。

下面是构造方法来初始化 Person 类,并调用其方法的例子:

const jack = new Person('Jack')
jack.sayHello() // Jack

静态方法

有时候,我们可能希望不创建对象,就直接调用类中的方法。

通过使用关键字 static,我们可以这样做。

使用 static 声明的方法也叫静态方法

下面是一个例子:

class Person {
  static sayHello() {
    console.log('Hello!')
  }
}

类中的方法被 static 修饰后,可以使用类名直接调用:

Person.sayHello()

get/set方法

类中除了普通方法静态方法构造方法外,还有 get 方法和 set 方法。

下面是 get/set 方法的一个简单例子:

class Person {
  constructor(name) {
    this.name = name; // 将调用set方法来设置name属性
  }
  
  get name() {
    return this._name;
  }
  
  set name(newName) {
    this._name = newName
  }
}
let jack = new Person();
jack.name = 'Jack'
console.log(jack.name) // Jack

总结

一个类中可以包含属性普通方法构造方法get/set 方法,当然,这些都是可选的,就像我们一开始定义一个空的类一样。

继承

在 JavaScript 中,类的继承就是一个类对另外一个类进行扩展。

要使用这种功能,我们需要使用到关键字 extends

下面是一个例子:

class Animal {
    eat() {
        console.log('eat......')
    }
}

class Cat extends Animal {
    super()
    miao() {
        console.log('miao, miao......')
    }
}

我们创建的猫类扩展了动物类。

我们把猫类称为动物类的子类,而动物类就是猫类的父类

现在,由于猫类继承(扩展)动物类,所以也有动物类的行为。

如下,我们可以调用父类的 eat() 方法:

const cat = new Cat()
cat.eat()
cat.miao()

同时,在子类可以通过 super 来调用父类

class Cat extends Animal {
    miao() {
        super.eat()
        console.log('miao, miao......')
    }
}
const cat = new Cat()
cat.miao()

上面程序将打印:eat…miao,miao…。

异步编程和回调

同步和异步

同步和异步其实是计算机世界里的一个基础概念。

同步可以简单地理解为在完成一条计算机指令之前,下一条指令不会被执行。

同步的一个问题其实是会造成程序执行被中断,从而进入等待。

比如我们浏览器需要调用网页里 JavaScript 文件中的一个函数,我们需要先下载这个文件。

否则我们浏览器将提示找不到这个函数。

但在下载这个文件期间,网页中的其他代码应该可以继续加载执行,而不是被下载这个 JavaScript 文件的行为所中断,造成用户的等待。

JavaScript 使用 回调 来解决这个问题。

回调的使用

介绍回调通常都是以一个简单的计时器开始。

以下是一个例子:

setTimeout(() => {
    console.log('2 秒后...')
  }, 2000)

setTimeout() 函数里有两个参数,一个是函数,另一个数字(单位是毫秒)。

语句 console.log(‘2 秒后…’) 将在两秒后被调用。

setTimeout() 并非是 JavaScript 核心语言的一部分,它并没有包括在 ECMAScript2020 手册中。

setTimeout() 一般由浏览器或者常见的 Node.js 环境所提供,所以在这里,2秒并不一定和你系统上的2秒相同,它取决于当时的运行环境。

我们再运行如下代码:

console.log('我还没来')
setTimeout(() => {
    console.log('2 秒后...')
  }, 2000)
console.log('我来了')

我们看到的结果可能是这样的:

我还没来
我来了
2 秒后...

回调的模式在处理网络、事件或者浏览器中的DOM的时候,都非常常见。

callback 的使用

处理回调我们通常会定义一个 callback 参数:

const foo = callback => {
  callback()
}

callback 通常是一个函数,下面是我们之前计算两数之和的一个例子,我们修改为使用回调函数:

function sum(num1, num2, callback) {
    setTimeout(() => callback(num1 + num2), 2000)
}
sum(1, 2, (num3) => console.log(`It\'s ${num3}`))

上面的程序将在2秒后输出 It’s 3

处理回调的成功和失败

在回调的模式中,其实是一个等待处理的过程,那么,处理的结果就有可能出现失败。

以下是回调时,处理成功和失败的例子:

function sum(num1, num2, success, failure) {
    setTimeout(() => {
        try {
            if (0 === num2) {
                throw '除数不能为0'
            }
            success(num1 / num2)
        } catch (e) {
            failure(e);
        }
    }, 1000)
}
const successCallback = (num3) => console.log(`成功: ${num3}`)
const failureCallback = (e) => console.log(`失败: ${e}`)

sum(6, 2, successCallback, failureCallback)
sum(6, 0, successCallback, failureCallback)

输出的结果如下:

成功: 3
失败: 除数不能为0

上面处理回调成功和失败的方式已经比较旧了,也不太推荐使用。

回调地狱

不太推荐使用上一节中处理回调的原因之一就是:在我们的回调需要依赖另一个回调时,就形成了嵌套的回调。

这将使我们的代码出现著名的回调地狱

我们看看把上面两数之和的例子变成回调地狱是什么样子:

function sum(num1, num2, success, failure) {
    setTimeout(() => {
        try {
            if (0 === num2) {
                throw '除数不能为0'
            }
            success(num1 / num2)
        } catch (e) {
            failure(e);
        }
    }, 1000)
}
// 没有嵌套回调的时候
// const successCallback = (num3) => console.log(`成功: ${num3}`)
// 回调地狱开始
const successCallback = (num3) => {
    sum(8, 4,  () => {
        sum(10, 5, () => {
            sum(12, 6, (anotherNum) => console.log(`Success: ${anotherNum}`))
        })
    });
}
const failureCallback = (e) => console.log(`失败: ${e}`)
sum(6, 2, successCallback, failureCallback)

我们输入 6 和 2 ,但最终,通过回调地狱,我们得到的是:

Success: 2

一旦代码中出现了 回调地狱 ,这份代码将非常难以维护。

为了解决回调地狱的问题, ES2015 也就是 ES6 推出了 Promises ,并且推出后,采用率就非常高,可以说成为了 JavaScript 中处理异步代码的标准。

Promises

promise 概述

promises 是处理异步的另外一种方式。

在 JavaScript 中,promises 较早期的使用应该是在 jQuery 或者 Dojo 的一些 API 中。

promises 规范有很多版本的实现,它在 2010 年开始慢慢普及。

对 JavaScript 来说,直到 ECMA2015 也就是 ES6 中才被正式引入。

现在几乎所有的现代浏览器都完全地支持了 Promises

promise 涉及了一些新的概念,就像我们学习对象和类一样。

promise 没有在我们的原始类型中,所以其实它是对象,更准确地说是一种引用类型。

创建 promise 对象

要创建一个 promise 其实和我们创建一个对象很相识:

let foo = new Promise(() => {});
// 普通函数的写法
// let foo = new Promise(function () {});

promise 中的状态

在使用 promise 之前,我们首先要了解 promise 对象中的一个属性叫做 state

state 用于描述 promise 中的三种状态:

  • pending:这是一种初始状态,不是成功也不是失败。
  • fulfilled:调用成功时的状态。
  • rejected:调用失败时的状态。

promise 的状态指明了 promise 是否开始执行了。

pending表示执行还没有开始或者还在执行中。

fulfilled 表示执行成功,没有出现异常。

rejected 表示没有执行成功,出现了异常。

其实状态在代码中是不会直接使用的,所以无需太在意状态的名称。

重点是知道有这三种状态。

在promise的状态中,还有一个概念是需要我们知道。

这个概念叫 settled(不变的)

settled 表示的是 fulfilledrejected 这两个中状态中的任一种。

promise 对象的状态,从 pending 转为 settled 后,这个 promise 对象的状态就不会再改变了。

promise 中的 resolve() 和 reject()

我们知道了 promise 有三种状态,但是 promise 的这三种状态其实是私有的,并没有公开让我们访问。

这样做的原因是,为了防止使用 promise 的时候直接根据状态来对对象进行同步编程。

虽然我们无法直接访问这三种状态,但 promise 封装了一个异步函数,称之为 executor,通过它,我们可以对 promise 的状态进行迁移。

JavaScript 自身为我们的 executor 提供了两个可选参数,一个是 resolve,另一个是 reject

所以,创建一个带有参数的参数的 executor 的例子如下:

let promise = new Promise((resolve, reject) => {
  // Do executor things
})

console.log(promise) // 将打印:Promise { <pending> }

上面的例子中,用于初始化 promise 对象的参数是一个函数,这个函数就是我们的 executor

如果要进行状态的转换,我们需要使用 JavaScript 为我们提供的两个回调:

  • resolve(value):成功且带有结果value
  • reject(evalue):失败且出现error

这个两个回到都可以在 executor 直接使用。

现在,让我们讨论一下状态是怎么变化的。

首先,executor 一开始处于 pending 状态,此时的返回值是 undefined

然后可以通过 resolve(value),将状态更改为 fulfilled,此时返回值是 value

而通过 reject(error),则将状态更改为 rejected,此时返回值是 error

下面,我们来看一个 resolve(value) 的例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})

下面是一个 reject(error) 的例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('foo')), 1000)
})

通常 executor 会使用异步执行,所以我们例子中用了 setTimeout(),但是其实并非必须的,直接写上执行的代码也可以:

let promise = new Promise((resolve, reject) => {
    resolve("ok")
})

另外,promise 的状态被转换后是无法再次修改的,也就是说一个 promise 一次只能改变一种状态:

let promise = new Promise((resolve, reject) => {
    resolve() // Promise <resolved>
    reject() // 将不会产生效果效果
})

promise 中的 .then、.catch 和 .finally

executor 用于执行我们的内容,那么我们如何处理执行后的结果呢?

为此,我们可以使用 promise 提供的 .then.catch.finally 方法。

.then

.then 方法中,有两个参数,两个参数都是函数。

一个用于接收 resolved 的结果,一个用于接收 rejected 的结果。

假设这两个方法为 onFulfilled 和 onRejected,则 .then 的例子如下:

promise.then(
  onFulfilled, 
  onRejected
);

我们可以选择只使用其中一个作为 .then 方法的参数,比如我们只对成功的结果感兴趣::

// 初始化一个 promise 对象
let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})
// 使用 .then 处理结果
promise.then(function (result) {console.log(result)})
// 在只有一行语句的时候,如果你掌握了箭头函数,选择使用箭头函数会更好
// promise.then(result => console.log(result))

上面的例子在一秒钟后,输出了 ok

其实 resolve 方法的参数并没有什么特别的规定。

但一般我们会把要传给回调函数的参数放进去,而 then 方法可以接收到这个参数值。

所以这里我们输出了 ok

.catch

.catch 其实只是 promise.then(undefined, onRejected) 的别名而已,使用 .catch 就相当于我们没有定义 .then 中的第一个参数。

下面是一个例子:

// 初始化一个 promise 对象
let promise = new Promise((resolve, reject) => {
    setTimeout(() => reject(new Error('foo')), 1000)
})
// 使用 .catch 处理结果
promise.catch(error => console.error(error))
// 上面的.catch等同于下面的.then
// promise.then(null, error => console.error(error))

.finally

许多编程语言都会有 finally 子句,JavaScript也一样。

.finally 通常用于进行一些收尾的工作,比如回收资源,停止加载指示符等等。

.finally 是无论结果如何,都需要执行的语句,以下是一个例子:

let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve('ok'), 1000)
})
promise.finally(() => console.log('All Done!'))

async 和 await

asyncawait 其实是 promise 的一种抽象或者说是 JavaScript 提供给我们的特殊语法,它们都是关键字,这使得在 JavaScript 进行异步编程更加优雅。

async

async 关键字可以用在函数声明、函数表达式、箭头函数和对象的方法中。

下面是一个例子:

async function foo() {
  return 'Hello'
}

这样就可以了!

接下来,我们就可以像使用 promise 一样地使用这个函数。

测试一下:

async function foo() {
    return 'Hello'
}
foo().then(result => console.log(result))

await

await 关键字的作用是让我们等待异步的动作完成并返回结果。

下面是一个例子:

async function foo() {
    let promise = new Promise((resolve, reject) => {
        setTimeout(() => resolve("ok"), 1000)
    })
    console.log(await promise) // 等 promise resolve 才执行
}
foo()

使用 await 需要注意的是,await 不能再普通函数中使用,只能在被 async 声明的函数中使用。

我们可以通过一个匿名的 async 函数来使用它,如:

(async () => {
  let foo = await fetch('test.json');
})

终章

非常感谢您阅读这本手册。

如无意外,本手册仍会更新。

希望本手册能帮助大家在学习 JavaScript 的道路上一路前行。

文晓欢欢。

2020.03.15

2人推荐
随时随地看视频
慕课网APP