专业编程基础技术教程

网站首页 > 基础教程 正文

JavaScript 初学者手册 javascript入门基础

ccvgpt 2024-12-17 11:52:28 基础教程 1 ℃



JavaScript 初学者手册 javascript入门基础

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

我相信这是您第一个绝佳选择的编程语言。

我们主要使用JavaScript来创建:

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

我们的网站:https://www.icoderoad.com

但 JavaScript 不限于这些东西,它还可以用于:

  • 使用 React Native 等工具创建移动应用程序
  • 为微控制器和物联网创建程序
  • 创建智能手表应用程序

它基本上可以做任何事情。它非常流行,以至于出现的所有新事物都会在某个时候能集成某种 JavaScript。

JavaScript 是一种编程语言,它是:

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

如您所知,JavaScript 与 Java 无关,这是一个糟糕的名称选择,但我们必须忍受它。

手册摘要

  1. 历史
  2. JavaScript
  3. 语法简介
  4. 分号
  5. 值域
  6. 变量
  7. 类型
  8. 表达式
  9. 运算符
  10. 优先规则
  11. 比较运算符
  12. 条件语句
  13. 数组
  14. 字符串
  15. 循环
  16. 函数
  17. 箭头函数
  18. 对象
  19. 对象属性
  20. 对象方法
  21. 继承
  22. 异步编程和回调
  23. Promise
  24. 异步和等待
  25. 变量范围
  26. 结论

历史

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

它是 Web 浏览器原生支持的第一种脚本语言,因此它获得了优于任何其他语言的竞争优势,今天它仍然是我们可以用来构建 Web 应用程序的唯一脚本语言。

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

最初 JavaScript 还没有今天那么强大,它主要用于花哨的动画和当时被称为动态 HTML奇迹。

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

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

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

JavaScript

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

例如,console.log()您可以在许多代码示例中找到的那行代码不是 JavaScript。相反,它是浏览器中提供给我们的庞大 API 库的一部分。

同样,在服务器上,有时很难将 JavaScript 语言功能与 Node.js 提供的 API 分开。

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

在本手册中,我将讨论 JavaScript 这种语言。

不会因外部生态系统提供的事物而使您的学习过程复杂化。

JavaScript 语法简介

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

  • 空白
  • 区分大小写
  • 文字
  • 身份标识
  • 注释

空白空间

JavaScript 不认为空白是有意义的。至少在理论上,可以以您可能喜欢的任何方式添加空格和换行符。

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

例如,我总是为每个缩进使用 2 个空格字符。

区分大小写

JavaScript 区分大小写。一个名为something的变量不同于Something.

其它标识符也是如此。

字面量

我们将字面量定义为在源代码中写入的值,例如,数字、字符串、布尔值或更高级的构造,如 Object 字面量 或 Array 字面量:

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

标识符

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

Test
test
TEST
_test
Test1
$test

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

有些名称是为 JavaScript 内部使用而保留的关键字,我们不能将它们用作标识符。

注释

注释是任何编程语言中最重要的部分之一。它们很重要,因为它们让我们可以注释代码并添加重要信息,否则其他人(或我们自己)阅读代码时无法获得这些信息。

在 JavaScript 中,我们可以使用//进行注释. JavaScript 解释器不会将//之后的所有内容视为代码。

像这样:

// a comment
true //another comment

另一种类型的注释是多行注释。它以 /*开头和*/结尾。

两者之间的所有内容均被认为注释:

/* some kind
of 
comment 

*/

分号

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

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

在大多数情况下,您可以在程序中完全省略分号,甚至无需考虑它。

这个事实是非常有争议的。一些开发人员将始终使用分号,而另一些开发人员则从不使用分号,并且您总是会发现使用分号的代码和不使用分号的代码。

我个人的偏好是避免使用分号,所以我在书中的示例不会包含它们。

值类型

字符串hello是一个。像一个数字12是一个

hello和12是值。string 和 number是这些值的类型

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

当我们需要一个值的引用时,我们将它分配给一个变量。变量可以有一个名称,而值是存储在变量中的内容,因此我们稍后可以通过变量名称访问该值。

变量

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

这是因为 JavaScript 是弱类型的,这是您经常听到的概念。

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

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

const a = 0

第二种方法是使用let

let a = 0

有什么不同?

const定义对值的常量引用。这意味着不能更改引用。不能为其重新分配新值。

使用letp声明的变量可以为其分配一个新值。

例如,不能这样做:

const a = 0
a = 1

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

另一方面,可以使用letp声明变量:

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

否则会收到“重复声明”错误。

我的建议是始终使用const声明产量,仅在您知道需要为该变量重新分配值时使用let。为什么?因为我们的代码的功能越少越好。如果我们知道一个值不能被重新分配,那么它就少了一个错误来源。

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

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

类型

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

它们是无类型的。

将具有某种类型的值分配给变量后,可以稍后重新分配该变量以承载任何其他类型的值,而不会出现任何问题。

在 JavaScript 中,我们有 2 种主要类型:原始类型对象类型

原始类型

原始类型是

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

还有两种特殊类型:nullundefined

对象类型

任何不是原始类型(字符串、数字、布尔值、null 或未定义)的值都是对象

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

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

表达式

表达式是 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

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

运算符

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

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

在对运算符的第一次介绍中,我们将介绍您最可能熟悉的运算符:具有 2 个操作数的运算符。

我在谈论变量时已经介绍了一个:赋值运算符=。您用=为变量赋值:

let b = 2

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

加法运算符 (+)

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

如果使用字符串,+运算符也会进行字符串连接,因此请注意:

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

减法运算符 (-)

const two = 4 - 2

除法运算符 (/)

返回第一个运算符和第二个运算符的商:

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

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

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

余数运算符 (%)

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

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

NaN余数总是为零,这是一个表示“非数字”的特殊值:

1 % 0 //NaN
-1 % 0 //NaN

乘法运算符 (*)

将两个数字相乘

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

幂运算符 (**)

将第一个操作数提高到第二个操作数的幂

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

优先规则

在同一行中包含多个运算符的每个复杂语句都会引入优先级问题。

举个例子:

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

比较运算符

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

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

比较运算符总是返回一个布尔值,即truefalse)。

这些是不等式比较运算符

  • <意思是“小于”
  • <=意思是“小于或等于”
  • >意思是“大于”
  • >=意思是“大于或等于”

例子:

let a = 2
a >= 1 //true

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

  • ===检查相等性
  • !==检查不平等

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

条件语句

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

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

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

if (true) {
  //do something
}

相反,这将永远不会执行:

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

条件检查传递给它的表达式的真值或假值。如果传递一个数字,除非它是 0,否则它总是计算为真。如果传递一个字符串,它总是计算为真,除非它是一个空字符串。这些是将类型转换为布尔值的一般规则。

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

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

if (true) doSomething()

但我总是喜欢用花括号,这样的语句更清楚。

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

if如果条件为假,则附加将要执行的语句:

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
}

数组

数组是元素的集合。

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

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

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 ]

如何将项目添加到数组

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

a.push(4)

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

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

如何从数组中删除一个项目

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

a.pop()

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

a.shift()

如何连接两个或多个数组

可以使用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]

如何在数组中查找特定项

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

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

返回第一个返回 true 的项目,如果未找到该元素则返回undefined

一个常用的语法是:

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

上面的行将返回数组中id === my_id的第一个元素。

findIndex()find()类似,但返回第一个为true 的项目的索引,如果未找到,则返回undefined

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

另一种方法是includes()

a.includes(value)

a如果包含则返回真value

a.includes(value, i)

如果在 position 之后包含value,则返回 true 。

字符串

字符串是一个字符序列。

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

'A string'
"Another string"

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

将字符串值分配给如下变量:

const name = 'Flavio'

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

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

''这是一个空字符串. 它的长度属性为 0:

''.length //0

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

"A " + "string"

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

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

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

使用反引号打开模板文字后,只需按 Enter 键即可创建一个新行,没有特殊字符,并且按原样呈现:

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 
  ${foo() ? 'x' : 'y'}`

循环

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

使用循环,我们可以自动化并重复一段代码,无论我们希望它运行多少次,甚至无限期地运行。

JavaScript 提供了许多迭代循环的方法。

我想专注于3种方式:

  • while 循环
  • for 循环
  • for..of 循环

while

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

我们在关键字之后添加一个条件while,并提供一个运行块,直到条件评估为true

例子:

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

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

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)

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循环一样,您可以使用 中断for循环,并且可以使用 快进到循环break的下一次迭代。forcontinue

for...of

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

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

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

函数

在任何中等复杂的 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. 在这种情况下,age函数内部是undefined.

我们可以使用以下条件检查值是否未定义:

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

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

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

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

虽然如果ageis或空字符串null,条件也将为真。0

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

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'
}

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

你也可以从一个函数中返回一个函数。

箭头函数

箭头函数是最近对 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
}

和常规函数一样,我们只能返回一个值。

箭头函数还可以包含其他箭头函数,甚至是常规函数。

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

对象

任何不是原始类型(字符串、数字、布尔值、符号、null 或未定义)的值都是对象

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

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'

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

对象属性

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

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

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

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

对象方法

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

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

在这个例子中,start属性分配了一个函数,我们可以使用我们用于属性的点语法来调用它,括号在末尾:

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

car.start()

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

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

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')

我们讨论了对象,这是 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

这有时非常有用。

继承

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

假设我们有一个类Person

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

我们可以定义一个新类,Programmer扩展Person

class Programmer extends Person {

}

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

const flavio = new Programmer()
flavio.hello() //'你好, 我是一个人。'

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

class Programmer extends Person {
  hello() {
    return super.hello() + 
      '我也是一名程序员。'
  }
}

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

上面的程序打印你好,我是一个人。我也是一名程序员。

异步编程和回调

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

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

一切都如您所愿,这是它在大多数编程语言中的工作方式。

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

不能只等待 2 秒钟来加载一个大文件,然后完全停止程序。

不能只等待下载网络资源后再做其他事情。

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

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

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

例子:

setTimeout(() => {
  // 2秒以后执行
  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,因此本手册中没有解释它们,但会在https://flaviocopes.com上的其他手册中找到很多示例。

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

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

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

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

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

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

Promise

Promise 是处理异步代码的另一种方法。

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

像这样:

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)
    })
  }) 
})

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

而不是这样做:

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')
    }
  }
)

我们可以将参数传递给我们想要的任何类型的解析和拒绝函数。

异步和等待

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

一个异步函数返回一个 Promise,如下例所示:

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

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

const data = await getData()

这样做,Promise返回的任何数据都将被分配给data变量。

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

有一个特别的警告:每当我们使用await关键字时,我们必须在定义为 async 的函数中这样做。

像这样:

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

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

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

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

例如,以下是使用 Fetch API 获取 JSON 资源并使用 Promise 解析它的方法:

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 => response.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 user.json() 
  return userData
}

getFirstUserData()

变量范围

当我介绍变量时,我谈到了使用constletvar

范围是对程序的一部分可见的变量集。

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

如果变量是在函数或块之外定义的,它会附加到全局对象并且它具有全局范围,这意味着它可以在程序的每个部分中使用。

let 、const 和var声明变量之间有一个非常重要的区别。

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

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

块是组合成一对花括号的一组指令,就像我们可以在if语句、for循环或函数中找到的指令一样。

重要的是要理解一个块没有为 定义一个新的范围var,但它为let和 const 定义了一个新的范围。

这具有非常实际的意义。

假设在函数的 if 条件内定义了一个 var 变量

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

如果调用此函数,ome data打印到控制台。

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

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

但是如果切换var datalet data

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

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

这是因为var是函数作用域,这里发生了一种特殊的事情,称为提升。简而言之,JavaScript 在运行代码之前将声明var移动到最近的函数的顶部。这或多或少是这个函数在 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 遵循相同的规则:它们是块范围的。

一开始可能会很棘手,但一旦你意识到这种差异,你就会明白为什么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

结论

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

我希望它能激发您更多地了解 JavaScript。

Tags:

最近发表
标签列表