Bootstrap

JavaScript入门指南(翻译自 The JS Handbook)

由于毕设要求翻译10000字的外文文献,所以机翻然后校正了下网上找到的 The JS Handbook,作为有基础的人花半天简单了解JavaScript的基础语法然后直接上手学前端框架还是不错的,但很多东西比如操作DOM元素等没讲,要系统的学JavaScript可能还是要自己翻菜鸟教程或者啃那本红皮的JavaScript高级程序设计。

原文网址为https://flaviocopes.com/book/js/,可以在线阅读或者下载PDF。

JavaScript手册

1. JavaScript简介

JavaScript 是世界上最流行的编程语言之一。

我相信它是一种很棒的语言,适合作为初学者学习的第一门编程语言。

我们主要使用JavaScript来创建

  • 网站
  • 网络应用程序
  • 使用 Node.js 的服务器端应用程序

但 JavaScript 的用途不仅限于这些,它还可以用来

  • 编写使用 React Native 等工具搭建的移动应用程序

  • 编写微控制器和物联网程序

  • 编写智能手表应用程序等等

它几乎可以到任何事情。 它是如此受欢迎,以至于现在的新事物几乎都在某种程度上对 JavaScript 进行了集成。

JavaScript 是一门具有如下特点的编程语言:

  • 高级:它进行了足够的抽象,允许您忽略运行它的机器的细节。 它使用垃圾收集器自动管理内存,因此您可以专注于代码,而不是像 C 等其他语言那样手动管理内存。它还提供许多数据结构,允许您处理更复杂的变量和对象。
  • 动态:与静态编程语言相反,动态语言在运行时执行许多静态语言在编译时执行的操作。 这有利有弊,它为我们提供了强大的功能,如动态类型、后期绑定、反射、函数式编程、对象运行时更改、闭包等等。 如果您不了解这些内容,请不要担心 - 您会在课程结束时了解所有这些内容。
  • 动态类型:变量不强制类型。 您可以将任何类型重新赋值给变量,例如,将整数赋值给保存字符串的变量。
  • 松散类型:与强类型相反,松散(或弱)类型语言不强制对象的类型,允许更多的灵活性但不提供类型安全和类型检查(TypeScript - 基于JavaScript构建 - 提供)
  • 解释性:它通常被认为是解释性语言,这意味着它与如C、Java或Go不同,在程序运行之前不需要编译阶段。 实际上,出于性能原因,浏览器会在执行 JavaScript 之前对其进行编译,但这对您来说是透明的:不涉及任何额外的步骤。
  • 多范式:该语言不强制执行任何特定的编程范式,这与强制使用面向对象编程的 Java 或强制使用命令式编程的 C 不同。 您可以使用面向对象的范式、使用原型和新的(从 ES6 开始)类语法来编写 JavaScript。 您可以使用函数式编程风格编写 JavaScript,使用其一流的功能,甚至可以使用命令式风格(类似 C)。

如果您想知道的话,JavaScript 与 Java 没有任何关系,JavaScript 是一个糟糕的名称选择,但我们只能接受它。

2. 一点点历史

JavaScript 创建于 1995 年,自其不起眼的开端以来已经走了很长一段路。

它是 Web 浏览器原生支持的第一种脚本语言,由于这一点,它比其他任何语言都有竞争优势,今天它仍然是我们用来构建Web应用程序的唯一脚本语言。

也存在其他语言,但都必须编译为 JavaScript - 或者最近的 WebAssembly,但这是另一回事了。

一开始,JavaScript 远没有今天那么强大,它主要用于精美的动画和实现当时令人惊叹的动态 HTML。

随着 Web 平台需求增长的要求(并将继续要求),JavaScript 也有必要随之发展,以适应世界上使用最广泛的生态系统之一的需求。

JavaScript现在在浏览器之外也被广泛使用。过去几年Node.js的兴起为后端开发打开了大门,而后端开发曾经是Java、Ruby、Python、PHP和更传统的服务器端语言的主场。

JavaScript 现在也是支持数据库和更多应用程序的语言,它甚至可以开发嵌入式应用程序、移动应用程序、电视机应用程序等等。一开始只是浏览器里的一种小语言的JavaScript现在已经成为世界上最流行的语言。

3. Just JavaScript

有时很难将 JavaScript 与其使用环境的特性区分开来。

例如,您可以在许多代码示例中找到的 console.log() 不是 JavaScript。 相反,它是浏览器中提供给我们的庞大 API 库的一部分。 同样地,在服务器上,有时很难将 JavaScript 语言特性与 Node.js 提供的 API 分开。

是 React 或 Vue 提供的特定功能? 还是通常所说的“纯 JavaScript”或“普通 JavaScript”?

在这本书中,我将讨论JavaScript这门语言,而不涉及外部生态系统提供的额外内容来使您的学习过程复杂化。

4. JavaScript 语法简介

在这个简短的介绍中,我想告诉您 5 个概念:

  • 空格
  • 大小写敏感
  • 字面量
  • 标识符
  • 注释

4.1. 空白

JavaScript 不认为空格有意义。理论上空格和换行符可以以任何你喜欢的方式添加,即使这只是理论上的。

在实践中,您很可能会保持定义明确的样式并遵守人们通常使用的样式,并通过 linter 或样式工具(如Prettier)保持强制执行。

例如,我喜欢总是使用 2 个字符来缩进。

4.2. 大小写敏感

JavaScript 大小写敏感。 名为 something 的变量不同于 Something

任何标识符也是如此。

4.3. 字面量

我们将写在源代码中的值定义为字面量,例如数字、字符串、布尔值或更高级的结构,比如对象字面量或数组字面量:

5
'Test'
true
['a', 'b']
{color: 'red', shape: 'Rectangle'}

4.4. 标识符

标识符 是可用于标识变量、函数或对象的字符序列。 它可以以字母、美元符号“$”或下划线“_”开头,并且可以包含数字。通过使用 Unicode,字母可以是任何允许的字符,例如,表情符号😄。

Test
test
TEST
_test
Test1
$test

美元符号通常用于引用 DOM 元素。

有些名称保留给 JavaScript 内部使用,我们不能将它们用作标识符。

4.5. 注释

注释是任何编程语言和程序中最重要的部分之一。它们很重要,因为它们允许我们注释代码并添加其他阅读代码的人(或我们自己)无法获得的重要信息。

在 JavaScript 中,我们可以使用 // 在单行上写注释。 // 之后的所有内容都不会被 JavaScript 解释器视为代码。

就像这样:

// a comment
true //another comment

另一种注释是多行注释。 它以 /* 开始,以 */ 结束。

介于两者之间的所有内容都不被视为代码:

/* some kind
of
comment

*/

5. 分号

JavaScript 程序中的每一行都可以选择使用分号终止。

我说可选,因为 JavaScript 解释器足够聪明,可以为您引入分号。

在大多数情况下,您可以从程序中完全省略分号。

这个事实是非常有争议的,你总是会发现使用分号的代码和不使用分号的代码。

我个人的偏好是除非绝对必要,否则始终避免使用分号。

6. 值

hello 字符串是一个。 像12这样的数字是一个

hello12 是值。 stringnumber 是这些值的类型

type 是值的种类,它的类别。 JavaScript中有许多不同的类型,我们将在后面详细讨论它们。每种类型都有自己的特点。

当我们需要引用一个值时,我们将其赋值给一个变量。 变量可以有一个名字,值存储在变量中,所以我们以后可以通过变量名访问该值。

7. 变量

变量是赋给标识符的值,因此您可以之后在程序中引用和使用它。

这是因为 JavaScript 是 松散类型,这是一个您经常听到的概念。

必须先声明变量,然后才能使用它。

我们有两种主要的方式来声明变量。 第一种是使用 const

const a = 0

第二种方法是使用 let

let a = 0

有什么不同?

const 定义对值的不变引用。这意味着不能更改引用。您不能为变量重新赋新值。

使用 let 则可以给变量赋一个新值。

例如,您不能这样做:

const a = 0
a = 1

因为你会得到一个错误:TypeError: Assignment to constant variable.

另一方面,您可以使用 let 来实现:

let a = 0
a = 1

const 并不像 C 等其他语言那样表示“常量”。 特别是,这并不意味着变量不能更改——这意味着它不能被重新赋值。 如果变量指向一个对象或数组(稍后我们将详细介绍对象和数组),则对象或数组的内容可以自由更改。

Const 变量必须在声明时初始化:

const a = 0

但是 let 值可以稍后初始化:

let a
a = 0

您可以在同一语句中一次声明多个变量:

const a = 1,
  b = 2
let c = 1,
  d = 2

但是您不能多次重新声明同一个变量:

let a = 1
let a = 2

否则您会收到“duplicate declaration”错误。

我的建议是始终使用 const,只有在知道需要为该变量重新赋值时才使用 let。 为什么? 因为我们的代码的功能越少越好。 如果我们知道一个值不能被重新赋值,那么它就少了一个bug来源。

现在我们已经了解了如何使用 constlet,我想提一下 var

直到2015年,var 是我们在JavaScript中声明变量的唯一方式。今天,现代代码库很可能只使用constlet。 我 在这篇文章中 详细介绍了一些根本差异,但如果你刚刚开始,你可能不需要关心它们。只需使用 constlet

8. 类型

JavaScript 中的变量没有附加任何类型。

它们是未类型化的。

一旦将某种类型的值赋给变量,以后可以将该变量重新赋值为承载任何其他类型的值,而不会出现任何问题。

在 JavaScript 中,我们有两种主要类型:基本类型对象类型

8.1. 基本类型

基本类型是

  • 数字
  • 字符串
  • 布尔值
  • 符号

还有两种特殊类型:nullundefined

8.2. 对象类型

任何不是基本类型(字符串、数字、布尔值、null 或 undefined)的值都是对象

对象类型有属性,也有可以作用于这些属性的方法

稍后我们将更多地讨论对象。

9. 表达式

表达式是 JavaScript 引擎可以计算并返回值的单个 JavaScript 代码单元。

表达式的复杂性可能会有所不同。

我们从非常简单的开始,称为基本表达式:

2
0.02
;('something')
true
false
this //the current scope
undefined
i //where i is a variable or a constant

算术表达式是接受一个变量和一个操作符(稍后会详细介绍操作符),并得到一个数字的表达式:

1 / 2
i++
i -= 2
i * 2

字符串表达式是结果为字符串的表达式:

'A ' + 'string'

逻辑表达式使用逻辑运算符并解析为布尔值:

a && b
a || b
!a

更高级的表达式涉及对象、函数和数组,稍后我会介绍它们。

10. 运算符

运算符允许您获得两个简单的表达式并将它们组合起来形成一个更复杂的表达式。

我们可以根据运算符使用的操作数对运算符进行分类。 一些运算符只能使用 1 个操作数。 大多数有 2 个操作数。 只有一个操作符可以处理3个操作数。

在第一次介绍运算符时,我们将介绍您最有可能熟悉的运算符:二元运算符。

我在讲变量的时候已经介绍过一个:赋值运算符 =。 您使用 = 为变量赋值:

let b = 2

现在让我们介绍另一组你们已经从基础数学中熟悉了的二进制运算符。

10.1. 加法运算符 (+)

const three = 1 + 2
const four = three + 1

如果您使用字符串,+ 运算符也可用作字符串连接,因此请注意:

const three = 1 + 2
three + 1 // 4
'three' + 1 // three1

10.2. 减法运算符 (-)

const two = 4 - 2

10.3. 除法运算符 (/)

返回第一个操作数和第二个操作数的商:

const result = 20 / 5 //result === 4
const result = 20 / 7 //result === 2.857142857142857

如果除以零,JavaScript 不会引发任何错误,但会返回 Infinity 值(如果值为负,则返回 -infinty )。

1 / 0 - //Infinity
  1 / 0 //-Infinity

10.4. 取余运算符(%)

在许多用例中,余数是一个非常有用的计算:

const result = 20 % 5 //result === 0
const result = 20 % 7 //result === 6

除以零的余数始终为 NaN,这是一个特殊值,意思是“非数字”:

1 % 0 //NaN

10.5. 乘法运算符 (*)

两个数相乘

1 * 2 //2
1 * -2 //-2

10.6. 求幂运算符 (**)

将第一个操作数取第二个操作数的幂

1 ** 2 //1
2 ** 1 //2
2 ** 2 //4
2 ** 8 //256
8 ** 2 //64

11. 优先级规则

每一个在同一行中有多个操作符的复杂语句都会遇到优先级问题。

举个例子:

let a = 1 * 2 + ((5 / 2) % 2)

结果是2.5,但为什么呢?

哪些操作先执行,哪些需要等待?

某些操作比其他操作具有更高的优先级。 下表列出了优先级规则:

操作符说明
* / %乘法/除法
+ -加法/减法
=赋值

同一级别的操作(如 +-)按照它们被发现的顺序从左到右执行。

遵循这些规则,上面的运算可以这样解决:

let a = 1 * 2 + ((5 / 2) % 2)
let a = 2 + ((5 / 2) % 2)
let a = 2 + (2.5 % 2)
let a = 2 + 0.5
let a = 2.5

12. 比较运算符

在赋值运算符和数学运算符之后,我要介绍的第三组运算符是比较运算符。

您可以使用以下运算符来比较两个数字或两个字符串。

比较运算符总是返回一个值为 truefalse 的布尔值。

这些是不等比较运算符

  • < 表示“小于”
  • <= 表示“小于或等于”
  • > 表示“大于”
  • >= 表示“大于或等于”

例子:

let a = 2
a >= 1 //true

除此之外,我们还有 4 个 等式运算符。 他们接受两个值,并返回一个布尔值:

  • === 检查是否相等
  • !== 检查是否不等

请注意,我们在 JavaScript 中也有 ==!=,但我强烈建议只使用 ===!== 因为它们可以防止一些细微的问题。

13. 条件语句

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

if 语句用于使程序根据表达式求值的结果选择一条路线或者另一条路线执行。

这是最简单的例子,它总是执行:

if (true) {
  //do something
}

相反,这永远不会执行:

if (false) {
  //do something (? never ?)
}

条件语句检查您传递给它的表达式值为 true 还是 false。 如果你传递一个数字,除非它是 0,否则它总是为 true。如果你传递一个字符串,它总是为 true,除非它是一个空字符串。 这些是将类型强制转换为布尔值的一般规则。

你注意到花括号了吗? 这称为,用于对不同语句列表进行分组。

块可以放在可以有单个语句的任何地方。如果你在条件语句之后只有一条语句要执行,你可以省略语句块,直接写语句:

if (true) doSomething()

但我总是喜欢用大括号,这样更清楚。

13.1. Else

您可以为 if 语句提供第二部分:else

附加一条语句,如果 if 条件为 false,将执行该语句:

if (true) {
  //do something
} else {
  //do something else
}

因为 else 接受一个语句,你可以在其中嵌套另一个 if/else 语句:

if (a === true) {
  //do something
} else if (b === true) {
  //do something else
} else {
  //fallback
}

14. 数组

数组是元素的集合。

JavaScript 中的数组本身不是一种类型。

数组是对象

我们可以用这两种不同的方式初始化一个空数组:

const a = []
const a = Array()

第一种是使用数组字面语法。第二个使用Array内置函数。

您可以使用以下语法预填充数组:

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

数组可以包含任何值,甚至是不同类型的值:

const a = [1, 'Flavio', ['a', 'b']]

由于我们可以将数组添加到数组中,因此我们可以创建多维数组,它具有非常有用的应用(例如矩阵):

const matrix = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
]

matrix[0][0] //1
matrix[2][0] //7

你可以通过引用它从0开始的索引来访问数组的任何元素:

a[0] //1
a[1] //2
a[2] //3

您可以使用以下语法用一组值初始化一个新数组,它首先初始化一个包含 12 个元素的数组,然后用数字 0 填充每个元素:

Array(12).fill(0)

您可以通过检查 length 属性来获取数组中元素的数量:

const a = [1, 2, 3]
a.length //3

请注意,您可以设置数组的长度。 如果您设置的长度大于数组的当前容量,则什么也不会发生。 如果您设置一个较小的长度,则数组会在该位置被截断:

const a = [1, 2, 3]
a //[ 1, 2, 3 ]
a.length = 2
a //[ 1, 2 ]

14.1 如何将项目添加到数组

我们可以使用 push() 方法在数组末尾添加一个元素:

a.push(4)

我们可以使用 unshift() 方法在数组的开头添加一个元素:

a.unshift(0)
a.unshift(-2, -1)

14.2. 如何从数组中删除项目

我们可以使用 pop() 方法从数组末尾删除一个项目:

a.pop()

我们可以使用 shift() 方法从数组的开头删除一个项目:

a.shift()

14.3. 如何连接两个或多个数组

您可以使用 concat() 连接多个数组:

const a = [1, 2]
const b = [3, 4]
const c = a.concat(b) //[1,2,3,4]
a //[1,2]
b //[3,4]

你也可以这样使用扩展运算符(...):

const a = [1, 2]
const b = [3, 4]
const c = [...a, ...b]
c //[1,2,3,4]

14.4. 如何在数组中查找特定项

您可以使用数组的 find() 方法:

a.find((element, index, array) => {
  //return true or false
})

此方法返回在提供的回调函数中返回 true 的第一个项目。 如果没有返回 true,它返回 undefined

定义回调函数的主体是您的责任,因此您就可以告诉 find() 您要查找的内容。

一个常用的语法是:

const my_id = 3

a.find((x) => x.id === my_id)

上面的代码将返回数组中 id 等于3的第一个元素,即 my_id 的值。

findIndex() 是另一个与 find() 类似的数组方法,但返回返回 true 的第一项的索引,如果未找到,则返回 undefined:

a.findIndex((element, index, array) => {
  //return true or false
})

另一个有用的方法是 includes()

a.includes(value)

如果 a 包含 value,则返回 true

a.includes(value, i)

如果 a 在位置 i 之后包含 value,则返回 true

15. 字符串

字符串是字符序列。

它也可以定义为字符串字面量,用引号或双引号括起来:

'A string'
'Another string'

我个人一直喜欢单引号,并且只在 HTML 中使用双引号来定义属性。

像这样给一个变量赋一个字符串值:

const name = 'Flavio'

你可以使用字符串的 length 属性来确定字符串的长度:

'Flavio'.length //6
const name = 'Flavio'
name.length //6

这是一个空字符串:''。 它的 `length`` 属性为0:

''.length //0

可以使用 + 运算符连接两个字符串:

'A ' + 'string'

您可以使用 + 运算符插入变量:

const name = 'Flavio'
'My name is ' + name //My name is Flavio

另一种定义字符串的方法是使用一种称为模板字面量的特殊语法,在反引号内定义。 它们对于简化多行字符串定义特别有用。 使用单引号或双引号,您需要使用转义字符,无法轻松定义多行字符串。

一旦用反引号打开模板字面量,你只需按回车键创建一个新的行,不带任何特殊字符,它就会按原样呈现:

const string = `Hey
this

string
is awesome!`

模板字面量也好在因为它们提供了一种将变量和表达式插入字符串的简单方法。

您可以使用 ${...} 语法来做到这一点:

const var = 'test'
const string = `something ${var}`
//something test

${} 中你可以添加任何东西,甚至是表达式:

const string = `something ${1 + 2 + 3}`
const string2 = `something
  ${a > b ? 'a' : 'b'}`

16. 循环

循环是 JavaScript 的主要控制结构之一。

有了循环,我们可以自动地无限地重复执行一段代码,我们想让它运行多少次就运行多少次。

JavaScript 提供了很多遍历循环的方法。

我想专注于 3 种方式:

  • while 循环
  • 循环
  • for…of 循环

16.1. while

while 循环是 JavaScript 为我们提供的最简单的循环结构。

我们在 while 关键字后面添加一个条件,并提供一个语句块,该块将一直运行直到条件求值为 false

例子:

const list = ['a', 'b', 'c']
let i = 0
while (i < list.length) {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
}

您可以使用 break 关键字中断 while 循环,如下所示:

while (true) {
  if (somethingIsTrue) break
}

如果您决定在循环中间要跳过当前迭代,则可以使用 continue 跳转到下一次迭代:

while (true) {
  if (somethingIsTrue) continue

  //do something else
}

while 非常相似,我们有 do..while 循环。 它与 while 基本相同,除了在代码块执行后再判断条件。

这意味着该块总是至少执行一次。

例子:

const list = ['a', 'b', 'c']
let i = 0
do {
  console.log(list[i]) //value
  console.log(i) //index
  i = i + 1
} while (i < list.length)

16.2. for

JavaScript 中第二个非常重要的循环结构是 for 循环

我们使用 for 关键字并传递一组共 3 条指令:初始化、条件和增量部分。

例子:

const list = ['a', 'b', 'c']

for (let i = 0; i < list.length; i++) {
  console.log(list[i]) //value
  console.log(i) //index
}

就像 while 循环一样,您可以使用 break 中断 for 循环,也可以使用 continue 快进到 for 循环的下一次迭代。

16.3. for…of

这个循环相对较新(2015 年引入),它是 for 循环的简化版本:

const list = ['a', 'b', 'c']

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

17. 函数

在任何中等复杂程度的 JavaScript 程序中,一切都发生在函数内部。

函数是 JavaScript 的核心,必不可少的部分。

什么是函数?

函数是一个独立的代码块。

这是一个函数声明

function getData() {
  // do something
}

一个函数可以在你想要的任何时候通过调用它来运行,就像这样:

getData()

一个函数可以有一个或多个参数:

function getData() {
  //do something
}

function getData(color) {
  //do something
}

function getData(color, age) {
  //do something
}

当我们可以传递参数时,我们调用传递参数的函数:

function getData(color, age) {
  //do something
}

getData('green', 24)
getData('black')

请注意,在第二次调用中,我将 black 字符串参数作为 color 参数传递,但没有传递 age。 在这种情况下,函数内的 ageundefined

我们可以使用以下条件检查值是否是 undefined

function getData(color, age) {
  //do something
  if (typeof age !== 'undefined') {
    //...
  }
}

typeof 是一个一元运算符,允许我们检查变量的类型。

您也可以通过以下方式检查:

function getData(color, age) {
  //do something
  if (age) {
    //...
  }
}

尽管在这种情况下,如果 agenull0 或空字符串,则条件将为false

您可以为参数设置默认值,以防它们未被传递:

function getData(color = 'black', age = 25) {
  //do something
}

您可以将任何值作为参数传递:数字、字符串、布尔值、数组、对象和函数。

一个函数有一个返回值。 默认情况下,函数返回 undefined,除非您添加一个带有值的 return 关键字:

function getData() {
  // do something
  return 'hi!'
}

我们调用该函数时,可以将该返回值赋给一个变量:

function getData() {
  // do something
  return 'hi!'
}

let result = getData()

result 现在保存了一个值为 hi! 的字符串。

您只能返回一个值。

要返回多个值,您可以返回一个对象或一个数组,如下所示:

function getData() {
  return ['Flavio', 37]
}

let [name, age] = getData()

函数可以在其他函数内部定义:

const getData = () => {
  const dosomething = () => {}
  dosomething()
  return 'test'
}

不能从闭包函数的外部调用嵌套函数。

您也可以从函数返回一个函数。

18. 箭头函数

箭头函数是最近才引入 JavaScript 的。

它们经常被用来代替我在上一章中描述的“常规”函数。 你会发现这两种形式无处不在。

在视觉上,它们允许您使用更短的语法编写函数,从:

function getData() {
  //...
}

;() => {
  //...
}

但是… 注意我们这里没有名字。

箭头函数是匿名的。 我们必须将它们赋给一个变量。

我们可以将一个普通函数赋值给变量,如下所示:

let getData = function getData() {
  //...
}

当我们这样做时,我们可以删除函数的名称:

let getData = function () {
  //...
}

并使用变量名调用函数:

let getData = function () {
  //...
}
getData()

这和我们处理箭头函数是一样的:

let getData = () => {
  //...
}
getData()

如果函数体只包含一条语句,则可以省略括号并将其全部写在一行中:

const getData = () => console.log('hi!')

参数在括号中传递:

const getData = (param1, param2) => console.log(param1, param2)

如果你有一个(而且只有一个)参数,你可以完全省略括号:

const getData = (param) => console.log(param)

箭头函数允许你有一个隐式返回:返回值而不必使用 return 关键字。

它在函数体中有一个在线语句时起作用:

const getData = () => 'test'

getData() //'test'

与常规函数一样,我们可以有默认参数:

您可以为参数设置默认值,以防它们未被传递:

const getData = (color = 'black', age = 2) => {
  //do something
}

我们只能返回一个值。

箭头函数可以包含其他箭头函数,也可以包含常规函数。

它们非常相似,所以您可能会问为什么要引入它们? 与常规函数的最大区别在于当它们被用作对象方法时。 这是我们很快就会研究的事情。

19. 对象

任何不是基本类型(字符串、数字、布尔值、符号、nullundefined)的值都是对象

下面是我们定义对象的方式:

const car = {}

这是对象字面语法,它是 JavaScript 中最好的东西之一。

您还可以使用 new Object 语法:

const car = new Object()

另一种语法是使用 Object.create()

const car = Object.create()

您还可以在带大写字母的函数之前使用 new 关键字来初始化对象。 此函数充当该对象的构造函数。 在这里,我们可以用接收的参数设置对象的初始状态:

function Car(brand, model) {
  this.brand = brand
  this.model = model
}

我们这样初始化一个新对象:

const myCar = new Car('Ford', 'Fiesta')
myCar.brand //'Ford'
myCar.model //'Fiesta'

对象总是通过引用传递

如果你给一个变量赋与另一个相同的值,如果它是像数字或字符串这样的基本类型,它们将按值传递:

举个例子:

let age = 36
let myAge = age
myAge = 37
age //36
const car = {
  color: 'blue',
}
const anotherCar = car
anotherCar.color = 'yellow'
car.color //'yellow'

即使是数组或函数,在底层也是对象,因此了解它们的工作原理非常重要。

19.1. 对象属性

对象具有属性,它们由与值关联的标签组成。

属性的值可以是任何类型,这意味着它可以是数组、函数,甚至可以是对象,因为对象可以嵌套其他对象。

这是我们在上一章看到的对象字面量语法:

const car = {}

我们可以这样定义一个 color 属性:

const car = {
  color: 'blue',
}

这里我们有一个 car 对象,它有一个名为 color 的属性,值为 blue

标签可以是任何字符串,但要注意特殊字符:如果我想在属性名中包含一个不能作为变量名使用的无效字符,我必须用引号括起来:

const car = {
  color: 'blue',
  'the color': 'blue',
}

无效的变量名字符包括空格、连接号和其他特殊字符。

如您所见,当我们有多个属性时,我们用逗号分隔每个属性。

我们可以使用 2 种不同的语法检索属性的值。

第一个是点号

car.color //'blue'

第二种(这是我们唯一可以用于具有无效名称的属性的方法)是使用方括号:

car['the color'] //'blue'

如果你访问一个不存在的属性,你会得到 undefined 值:

car.brand //undefined

如前所述,对象可以将嵌套对象作为属性:

const car = {
  brand: {
    name: 'Ford',
  },
  color: 'blue',
}

在此示例中,您可以这样访问品牌名称:

car.brand.name

或者

car['brand']['name']

您可以在定义对象时设置属性的值。

但是您以后可以随时更新它:

const car = {
  color: 'blue',
}

car.color = 'yellow'
car['color'] = 'red'

您还可以向对象添加新属性:

car.model = 'Fiesta'

car.model //'Fiesta'

给定对象

const car = {
  color: 'blue',
  brand: 'Ford',
}

您可以这样删除此对象的属性

delete car.brand

19.2. 对象方法

我在前一章中谈到了函数。

函数可以被赋给函数属性,在这种情况下,它们被称为方法

在此示例中,start 属性被赋值了一个函数,我们可以通过使用访问属性的点语法调用它,并在末尾加上圆括号:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function () {
    console.log('Started')
  },
}

car.start()

在使用 function() {} 语法定义的方法中,我们可以通过引用 this 访问对象实例。

在以下示例中,我们可以使用 this.brandthis.model 访问 brandmodel 属性值:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: function () {
    console.log(`Started
      ${this.brand} ${this.model}`)
  },
}

car.start()

注意常规函数和箭头函数之间的区别是很重要的:如果我们使用箭头函数,我们无法访问 this

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  start: () => {
    console.log(`Started
      ${this.brand} ${this.model}`) //not going to work
  },
}

car.start()

这是因为箭头函数没有绑定到对象

这就是为什么常规函数经常被用作对象方法的原因。

方法可以接受参数,如常规函数:

const car = {
  brand: 'Ford',
  model: 'Fiesta',
  goTo: function (destination) {
    console.log(`Going to ${destination}`)
  },
}

car.goTo('Rome')

20. 类

我们讨论了对象,它是 JavaScript 中最有趣的部分之一。

在本章中,我们将更上一层楼,介绍类。

什么是类? 它们是为多个对象定义通用模式的一种方式。

让我们来看一个人对象:

const person = {
  name: 'Flavio',
}

我们可以创建一个名为 Person 的类(注意大写的 P,这是使用类时的约定),它有一个 name 属性:

class Person {
  name
}

现在从这个类中,我们初始化一个 flavio 对象,如下所示:

const flavio = new Person()

flavio 被称为 Person 类的实例。

我们可以设置 name 属性的值:

flavio.name = 'Flavio'

我们可以这样访问它的值:

flavio.name

就像我们处理对象属性那样。

类可以包含属性,例如 name,和方法。

方法以这种形式定义:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}

我们可以在类的实例上调用方法:

class Person {
  hello() {
    return 'Hello, I am Flavio'
  }
}
const flavio = new Person()
flavio.hello()

有一个名为 constructor() 的特殊方法,我们可以在创建新对象实例时使用它来初始化类属性。

它是这样工作的:

class Person {
  constructor(name) {
    this.name = name
  }

  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}

请注意我们如何使用 this 来访问对象实例。

现在我们可以从类实例化一个新对象,传递一个字符串,当我们调用 hello 时,我们会得到一条个性化消息:

const flavio = new Person('flavio')
flavio.hello() //'Hello, I am flavio.'

初始化对象时将调用 constructor() 方法,并传递参数。

通常方法是在对象实例上定义的,而不是在类上。

您可以将方法定义为 static 以允许它在类上执行:

class Person {
  static genericHello() {
    return 'Hello'
  }
}

Person.genericHello() //Hello

这有时非常有用。

21. 继承

一个类可以继承另一个类,使用该类初始化的对象继承两个类的所有方法。

假设我们有一个 Person 类:

class Person {
  hello() {
    return 'Hello, I am a Person'
  }
}

我们可以定义一个继承 Person的新类 Programmer

class Programmer extends Person {}

现在,如果我们用类 Programmer 实例化一个新对象,它可以访问 hello() 方法:

const flavio = new Programmer()
flavio.hello() //'Hello, I am a Person'

在子类中,您可以通过调用 super() 来引用父类:

class Programmer extends Person {
  hello() {
    return super.hello() + '. I am also a programmer.'
  }
}

const flavio = new Programmer()
flavio.hello()

上面的程序打印 “Hello, I am a Person. I am also a programmer.”。

22. 异步编程和回调

大多数时候,JavaScript 代码是同步运行的。

这意味着执行一行代码,然后执行下一行,依此类推。

一切都如您所料,在大多数编程语言中也是如此。

但是,有时您不能只是等待一行代码执行。

你不能完全停止程序,等待 2 秒加载一个大文件。

你不能在执行其他操作前,只是等待网络资源被下载。

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

如何使用回调的最简单示例之一是计时器。 计时器不是 JavaScript 的一部分,但它们是由浏览器和 Node.js 提供的。 让我谈谈我们拥有的计时器之一:setTimeout()

setTimeout() 函数接受 2 个参数:一个函数和一个数字。 该数字是运行函数之前必须经过的毫秒数。

例子:

setTimeout(() => {
  // runs after 2 seconds
  console.log('inside the function')
}, 2000)

包含 console.log('inside the function') 行的函数将在 2 秒后执行。

如果您在函数之前添加一个 console.log('before'),并在它之后添加一个 console.log('after')

console.log('before')
setTimeout(() => {
  // runs after 2 seconds
  console.log('inside the function')
}, 2000)
console.log('after')

您将在控制台中看到这样的情况:

before
after
inside the function

回调函数是异步执行的。

在处理文件系统、网络、事件或浏览器中的 DOM 时,这是一种非常常见的模式。

我提到的所有内容都不是“核心”JavaScript,因此本手册中未对它们进行解释,但你可以在flaviocopes.com上找到我的其他手册中的很多例子。

以下是我们如何在代码中实现回调。

我们定义了一个接受一个callback参数的函数,该参数是一个函数。

当代码准备好调用回调时,我们通过传递结果来调用它:

const doSomething = (callback) => {
  //do things
  //do things
  const result = /* .. */ callback(result)
}

使用此函数的代码将像这样使用它:

doSomething((result) => {
  console.log(result)
})

23. Promises

我感觉这一节和下一节写的挺烂的,不如看JavaScript Promise _ 菜鸟教程

Promises 是处理异步代码的另一种方式。

正如我们在上一章中看到的,通过回调我们将一个函数传递给另一个函数调用,该函数将在函数完成处理时调用。

像这样:

doSomething((result) => {
  console.log(result)
})

doSomething() 代码结束时,它调用作为参数接收的函数:

const doSomething = (callback) => {
  //do things
  //do things
  const result = /* .. */ callback(result)
}

这种方法的主要问题是如果我们需要在我们的其余代码中使用这个函数的结果,我们所有的代码都必须嵌套在回调中,如果我们必须做2-3个回调,就会陷入通常所说的“回调地狱”,多级函数缩进到其他函数中:

doSomething((result) => {
  doSomethingElse((anotherResult) => {
    doSomethingElseAgain((yetAnotherResult) => {
      console.log(result)
    })
  })
})

Promises 是处理这个问题的一种方法。

代替这样:

doSomething((result) => {
  console.log(result)
})

我们以这种方式调用基于 promise 的函数:

doSomething().then((result) => {
  console.log(result)
})

我们首先调用该函数,然后我们有一个 then() 方法,该方法在函数结束时被调用。

缩进并不重要,但为了清晰起见,您通常会使用这种样式。

使用 catch() 方法检测错误很常见:

doSomething()
  .then((result) => {
    console.log(result)
  })
  .catch((error) => {
    console.log(error)
  })

现在,为了能够使用这种语法,doSomething() 函数的实现必须有点特殊。 它必须使用 Promises API。

而不是将其声明为普通函数:

const doSomething = () => {}

我们将其声明为一个 promise 对象:

const doSomething = new Promise()

然后我们在 Promise 构造函数中传递一个函数:

const doSomething = new Promise(() => {})

该函数接收 2 个参数。 第一个是我们调用来解析 promise 的函数,第二个是我们调用来拒绝 promise 的函数。

const doSomething = new Promise((resolve, reject) => {})

解析 promise 意味着成功完成它(这导致调用使用它的 then() 方法)。

拒绝 promise 意味着以错误结束它(这会导致调用 catch() 方法)。

就是这样:

const doSomething = new Promise(
  (resolve, reject) => {
    //some code
    const success = /* ... */
    if (success) {
      resolve('ok')
    } else {
      reject('this error occurred')
    }
  }
)

我们可以将我们想要的任何类型的参数传递给 resolvereject 函数。

24. 异步和等待

异步函数是对 promises 的更高层次的抽象。

异步函数返回一个 promise,如本例所示:

const getData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve('some data'), 2000)
  })
}

任何想要使用此函数的代码都将在该函数之前使用 await 关键字:

const data = await getData()

这样做,promise 返回的任何数据都将赋值给 data 变量。

在我们的例子中,数据是“some data”字符串。

有一个特别的警告:无论何时我们使用 await 关键字,我们都必须在定义为 async 的函数中使用。

像这样:

const doSomething = async () => {
  const data = await getData()
  console.log(data)
}

Async/await 组合让我们拥有更简洁的代码和简单的思维模型来处理异步代码。

正如您在上面的示例中看到的,将其与使用 promises 或回调函数的代码进行比较,我们的代码看起来非常简单。

这是一个非常简单的例子,当代码复杂得多时,主要的好处就会出现。

例如,下面是如何使用 Fetch API 获取 JSON 资源,并使用 promises 解析它:

const getFirstUserData = () => {
  // get users list
  return (
    fetch('/users.json')
      // parse JSON
      .then((response) => response.json())
      // pick first user
      .then((users) => users[0])
      // get user data
      .then((user) => fetch(`/users/${user.name}`))
      // parse JSON
      .then((userResponse) => userResponse.json())
  )
}

getFirstUserData()

这是使用 await/async 提供的相同功能:

const getFirstUserData = async () => {
  // get users list
  const response = await fetch('/users.json')
  // parse JSON
  const users = await response.json()
  // pick first user
  const user = users[0]
  // get user data
  const userResponse = await fetch(`/users/${user.name}`)
  // parse JSON
  const userData = await userResponse.json()
  return userData
}

getFirstUserData()

25. 变量作用域

在介绍变量的时候,我谈到了使用 constletvar

作用域是对程序的一部分可见的一组变量。

在 JavaScript 中,我们有全局作用域、块作用域和函数作用域。

如果一个变量是在函数或块之外定义的,它附属于全局对象并且具有全局作用域,这意味着它在程序的每个部分都可用。

varletconst 声明之间有一个非常重要的区别。

在函数内定义为 var 的变量仅在该函数内可见。 类似于函数参数。

另一方面,定义为 constlet 的变量仅在定义它的内可见。

块是一组用花括号括起来的指令,就像我们在 if语句或 for 循环还有函数中看到的那样。

重要的是要理解块不为 var定义新的作用域,但为 letconst 定义了新的作用域。

这具有非常实际的意义。

假设您在函数的 if 条件语句中定义了一个 var 变量

function getData() {
  if (true) {
    var data = 'some data'
    console.log(data)
  }
}

如果你调用这个函数,你会得到打印到控制台的“some data”。

如果您尝试移动 console.log(data)if之后,它仍然有效:

function getData() {
  if (true) {
    var data = 'some data'
  }
  console.log(data)
}

但是,如果您将 var data 切换为 let data

function getData() {
  if (true) {
    let data = 'some data'
  }
  console.log(data)
}

你会得到一个错误:ReferenceError: data is not defined

这是因为 var 是函数作用域的,这里发生了一件称为提升的特殊事情。 简而言之,var 声明在运行代码之前被 JavaScript 移动到最近函数的顶部。 在JS看来函数内部差不多是这样:

function getData() {
  var data
  if (true) {
    data = 'some data'
  }
  console.log(data)
}

这就是为什么甚至在它被声明之前,你也可以在函数的顶部使用 console.log(data),你会得到 undefined 作为该变量的值:

function getData() {
  console.log(data)
  if (true) {
    var data = 'some data'
  }
}

但是如果你切换到 let ,你会得到一个错误 ReferenceError: data is not defined ,因为提升不会发生在 let 声明上。

const 遵循与 let 相同的规则:它是块作用域的。

一开始它可能会很棘手,但是一旦你意识到这种差异,你就会明白为什么与 let 相比 var 现在被认为是一种不好的做法:它们确实有更少的活动部分,并且它们的作用域仅限于块,这也使它们成为非常好的循环变量,因为它们在循环结束后就不再存在了:

function doLoop() {
  for (var i = 0; i < 10; i++) {
    console.log(i)
  }
  console.log(i)
}

doLoop()

当您退出循环时,i 将是一个值为 10 的有效变量。

如果你切换到 let,尝试 console.log(i) 将导致错误 ReferenceError: i is not defined

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;