Files
blog-press/docs/Web-Front/Others/JavaScript-Guide.md
2025-12-12 10:41:15 +08:00

1762 lines
54 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# JavaScript简介
## JavaScript 实现
  完整的`JavaScript`实现包含以下几个部分:
1. 核心(`ECMAScript`)
2. 文档对象模型(`DOM`)
3. 浏览器对象模型(`BOM`)
  `ECMAScript`只是一个语言规范的标准,而`JavaScript`实现了这一标准。
  文档对象模型`DOM`(`Document Object Model`)是一个应用编程接口,用于在`HTML`中使用扩展的`XML``DOM`将整个页面抽象为一组分层节点。
  浏览器对象模型用于支持访问和操作浏览器的窗口。
# 语言基础
## 变量
### var let const
* `let`声明的范围是块作用域,`var`声明的范围是函数作用域。块作用域是函数作用域的子集。
  `var``let`的区别:
  `var`:函数作用域;存在变量提升;可重复定义;声明的变量会作为`window`的属性。
  `let`:块级作用域;不存在变量提升(有暂时性死区);不可重复定义;声明的变量不会作为`window`的属性。
  块级作用域:即在{}花括号内的域,由{ }包括,比如`if{}`块、`for(){}`块。
  函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体都是有定义的。
  暂时性死区:在代码块中,在声明变量之前,该变量是不可用的。
* `const`的行为与`let`基本相同,唯一的重要区别为声明变量的同时必须初始化变量。
* 定义和声明变量时,`const`优先,`let`次之。
## 数据类型
  `ECMAScript`变量包含两种类型的数据:**原始值**和**引用值**。
  原始值就是简单的数据类型,引用值则是由多个值构成的对象。**保存原始值的变量是按值访问的**,实际操作的就是存储在变量中的实际值。而引用值是保存在内存中的对象,`JavaScript`不允许直接访问内存位置,因此操作对象(引用值)时,实际操作的是对该对象的引用,因此**保存引用值的变量是按引用访问的**。
### 原始值
  原始值一共有6种`Undefined、Null、Boolean、Number、String和Symbol`
# 集合引用类型
## Object
* 对象字面量表达式中,属性名可以是字符串或数值,数值属性会自动转换为字符串。
* 使用中括号可以通过变量访问属性,也可以在属性名中包括可能会导致语法错误的字符。
```js
let person = {
'total name': 'Cxx',
5: true
}
console.log(person['total name'])
```
## Array
* `from()`用于将类数组结构转换为数组实例,`of`用于将一组参数转换为数组实例。
```js
console.log(Array.from('Cxx'))
const a = [1, 2, 3, 4]
console.log(Array.from(a, x => x ** 2))
console.log(Array.of(1, 2, 3, 4, 'c', 'x', 'x'))
// 输出
// > ["C","x","x"]
// > [1,4,9,16]
// > [1,2,3,4,"c","x","x"]
```
* 如果把一个值设置给超过数组最大索引的索引,则数组长度会自动扩展到该索引值`+1`。通过修改`length`属性,可以从数组末尾删除或添加元素。
* 检测一个值是否为数组:`Array.isArray()`
* `keys()`返回数组索引的迭代器,`values()`返回数组元素的迭代器,`entries()`返回索引/值对的迭代器。
```js
const a = ['foo', 'bar']
// 对象解构
for (const [idx, element] of a.entries()) {
console.log(idx, element)
}
// 输出
// > 0,foo
// > 1,bar
```
### 数组方法
#### 基本方法
* 栈方法:
```js
// 接收任意数量的参数,并将其添加至数组末尾,返回数组的最新长度
arrayLength = array.push(...item)
// 删除数组的最后一项并减少数组的length值返回被删除的项
deleteItem = array.pop()
```
* 队列方法:
```js
// 删除数组的第一项并返回它,然后数组长度-1
deleteItem = array.shift()
// 在数组开头添加任意多个值,然后返回新的数组长度
arrayLength = array.unshift(...item)
```
* 排序方法:
```js
// 将数组元素反向排列
array = array.reverse()
// 按照升序重新排列数组元素
// 会先对每一项调用 String() 转换为字符串,再进行排序
array = array.sort([compareFunction])
// 示例
const a = [0, 1, 5, 10, 15]
function compare(value1, value2) {
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
console.log(a.sort())
console.log(a.sort(compare))
// 输出
// > [0,1,10,15,5]
// > [0,1,5,10,15]
```
#### 操作方法
* concat()
```js
// 将其参数添加至副本末尾 然后返回这个新数组
newArray = array.concat(item/array)
// 示例
const colors1 = ['red', 'green']
const colors2 = ['black', 'white']
console.log(colors1.concat(colors2))
// 是否打平数组
colors2[Symbol.isConcatSpreadable] = false
console.log(colors1.concat(colors2))
// 输出
// > ["red","green","black","white"]
// > ["red","green",["black","white"]]
```
* slice():切片
```js
// 参数:返回元素的开始索引和结束索引 返回值:对应索引的元素
newArray = array.slice(startIndex, [endIndex])
// 示例
const colors = ['red', 'green', 'black', 'white']
console.log(colors.slice(2))
// 不改变原数组
console.log(colors)
console.log(colors.slice(1, 3))
// 输出
// > ["black","white"]
// > ["red","green","black","white"]
// > ["green","black"]
```
* splice():拼接
```js
// 参数:索引值 需要删除的个数(可以为0) 需要插入的元素(可以为0)
// 返回值:从数组中删除的元素
deleteArray = array.splice(index, deleteNum, [insertItem])
const colors = ['red', 'green', 'black', 'white']
// 删除元素
const deleteArray = colors.splice(0, 1)
console.log(deleteArray)
console.log(colors)
console.log('**********')
// 插入元素
const insertArray = colors.splice(1, 0, 'yellow')
console.log(insertArray)
console.log(colors)
console.log('**********')
// 替换元素
const replaceArray = colors.splice(1, 1, 'orange')
console.log(replaceArray)
console.log(colors)
// 输出
// > ["red"]
// > ["green","black","white"]
// > **********
// > []
// > ["green","yellow","black","white"]
// > **********
// > ["yellow"]
// > ["green","orange","black","white"]
```
#### 搜索方法
&emsp;&emsp;`indexOf()``lastIndexOf()``includes()`
```js
// 参数:要查找的元素,搜索位置 返回值:查找元素在数组的位置
// 从第一项向后搜索
index = indexOf(searchItem, [searchInedex])
// 从最后一项向前搜索
index = lastIndexOf(searchItem, [searchInedex])
// 参数:同上 返回值:是否找到一个与指定元素匹配的项
isInclude = includes(searchItem, [searchInedex])
```
&emsp;&emsp;自定义搜索:`find()``findIndex()`
```js
// 参数:断言函数(元素、索引和数组本身) 返回值:匹配项
item = array.find((element, index, array) => { ... })
// 参数:同上 返回值:匹配项的索引
itemIndex = array.findIndex((element, index, array) => { ... })
// 示例
const people = [
{
name: 'Cxx',
age: 27
},
{
name: 'Pjm',
age: 25
}
]
console.log(people.find((element, index, array) => element.age > 26))
console.log(people.findIndex((element, index, array) => element.age > 26))
// 输出
// > {"name":"Cxx","age":27}
// > 0
```
#### 迭代方法
&emsp;&emsp;`every()``filter()``forEarch()``map()``some()`。每个方法都接收一个**以每一项为参数运行的函数**。该函数接收3个参数数组元素、元素索引和数组本身。
* every():如果每一项返回`true`,则返回`true`
* some():如果某一项返回`true`,则返回`true`
```js
let numbers = [1, 2, 3, 4, 5]
console.log(numbers.every((item, index, array) => item > 3))
console.log(numbers.some((item, index, array) => item > 3))
// 输出
// > false
// > true
```
* filter():返回每次函数调用的结果为`true`的项组成的数组
* map():返回每次函数调用的结果组成的数组
```js
let numbers = [1, 2, 3, 4, 5]
console.log(numbers.filter((item, index, array) => item > 3))
console.log(numbers.map((item, index, array) => item * 3))
```
* forEach():循环遍历,不返回结果
```js
let numbers = [1, 2, 3, 4, 5]
numbers.forEach((item, index, array) => {
console.log(index, item)
})
// 输出
// > 0,1
// > 1,2
// > 2,3
// > 3,4
// > 4,5
```
## Map
### 基本API
&emsp;&emsp;使用`new``Map`构造函数就可以创建一个空映射,也可以给**`Map`构造函数传入一个可迭代对象进行实例化。**
&emsp;&emsp;`Map`可以使用任何`JavaScript`数据类型作为键。
&emsp;&emsp;映射实例可以提供一个迭代器,并且会维护键值对的插入顺序。
```js
// 创建空映射
const m = new Map()
// 使用数组初始化映射
const m1 = new Map([['key1', 'value1'], ['key2', 'value2']])
// 使用迭代器初始化映射
const m2 = new Map({
[Symbol.iterator]: function* () {
yield ['key1', 'value1']
yield ['key2', 'value2']
}
})
// 常用API
console.log(m1.has('key1')) // 查询
console.log(m1.get('key3')) // 查询
m1.delete('key1') // 删除某个键
console.log(m1.get('key'))
m1.set('key3', 'value3') // 增加某个键
console.log(m1.get('key3'))
m1.clear()
console.log(m1.size) // 获取键值对数量
console.log('**********')
// Map迭代 API同迭代器
for (const pair of m2.entries()) {
console.log(pair)
}
// 输出
// > true
// > undefined
// > undefined
// > value3
// > 0
// > **********
// > ["key1","value1"]
// > ["key2","value2"]
```
### Object与Map区别
| | Object | Map |
| :-----:| :----: | :----: |
| 内存占用 | 多 | **少** |
| 插入性能 | 差 | **强** |
| 查询速度 | **快** | 慢 |
| 删除性能 | 差 | **强** |
### WeakMap
* 弱映射键只能是`Object`或者继承自`Object`的类型。
* 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的,不能遍历。
## Set
### 基本API
```js
// 创建空映射
const s = new Set()
// 使用数组初始化映射
const s1 = new Set(['value1', 'value2'])
// 使用迭代器初始化映射
const s2 = new Set({
[Symbol.iterator]: function* () {
yield 'value1'
yield 'value2'
}
})
// 常用API
console.log(s1.has('value1')) // 查询
s1.delete('value1') // 删除某个值
console.log(s1.has('value1'))
s1.add('value3') // 增加某个值
console.log(s1.has('value3'))
s1.clear()
console.log(s1.size) // 获取元素数量
console.log('**********')
// Set迭代 API同迭代器
for (const value of s2.values()) {
console.log(value)
}
// 输出
// > true
// > false
// > true
// > 0
// > **********
// > value1
// > value2
```
### WeakSet
* 成员只能是`Object`或者继承自`Object`的类型。
* 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存`DOM`节点,不容易造成内存泄漏。
* 不能遍历。
### 总结
&emsp;&emsp;`Map`是键-值对,`Set`是值-值对。
# 迭代器与生成器
## 迭代器
### 迭代器概念
&emsp;&emsp;可迭代对象:实现了正式的`Iterable`接口,而且可以通过迭代器`Iterator`消费。
&emsp;&emsp;实现`Iterable`接口:需要同时具备**支持迭代的自我识别能力和创建实现`Iterator`接口的对象的能力**。(也即必须暴露一个`Symbol.iterator`属性作为默认迭代器)
&emsp;&emsp;迭代器:是一个可以由任意对象实现的接口,支持连续获取对象产出的每一个值。任何实现`Iterable`接口的对象都有一个`Symbol.iterator`属性,这个属性引用默认迭代器。默认迭代器就像一个迭代器工厂,也是一个函数,调用之后就会产生一个实现`Iterator`接口的对象。
&emsp;&emsp;迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象,而迭代器会暴露迭代其关联可迭代对象的`API`
&emsp;&emsp;**实现`Symbol.iterator`方法的对象就是可迭代对象,该方法返回一个迭代器。实现了`next`方法的对象就是迭代器** 可迭代对象和迭代器可以合并为一个对象,即同时实现`Symbol.iterator``next`方法。
&emsp;&emsp;可迭代对象是按一定规则建立好的对象(函数或者其他),迭代器则是需要的时候实例化即可,每个迭代器都拥有一定的`API`
### 迭代器使用
&emsp;&emsp;接收可迭代对象的原生语言特性:
&emsp;&emsp;1.`for-of`循环 2.数组解构 3.扩展操作符 4.`Array.from()` 5.创建集合 6.创建映射 7.`yield*`操作符,在生成器中使用。
&emsp;&emsp;迭代器`API`使用`next()`在可迭代对象中遍历数据,返回`done(true/false)``value`值。
&emsp;&emsp;只要迭代器到达`done:true`状态,后续调用`next()`就一直返回同样的值。不同迭代器的实例相互之间没有联系,只会独立遍历可迭代对象。如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。
### 原生可迭代对象类型的迭代器
```js
let arr = ['foo', 'bar']
// arr[Symbol.iterator] 是迭代器工厂函数
// 调用该函数即可生成 迭代器
let iter = arr[Symbol.iterator]()
console.log(iter.next())
```
&emsp;&emsp;实现`Iterable`接口的内置类型:
&emsp;&emsp;1.字符串 2.数组 3.映射 4.集合 5.`arguments`对象 6.`NodeList``DOM`集合类型
### 自定义迭代器
&emsp;&emsp;在函数/对象中实现`next()`接口即可构成迭代器,但此时只能调用迭代器`API`,无法使用可迭代对象的特性,如`for-of`循环。
```js
function makeRangeIterator(start = 0, end = Infinity, step = 1) {
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
// 实现 next 方法
next: function() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false }
nextIndex += step;
iterationCount++;
// 必须返回 value done 结构的对象
return result;
}
return { value: iterationCount, done: true }
}
};
return rangeIterator;
}
// 调用迭代器函数
let iterator = makeRangeIterator(1, 10, 2);
// 可以使用迭代器API
let result = iterator.next();
while (!result.done) {
console.log(result.value);
result = iterator.next();
}
// 无法使用可迭代对象的特性
for (let it of iterator) {
console.log(it)
}
// 输出
// 1
// 3
// 5
// 7
// 9
// Uncaught TypeError: iterator is not iterable
```
### 自定义可迭代对象
&emsp;&emsp;如果需要使用可迭代对象的特性,则需将其改为对象模式,并实现`Symbol.iterator`属性。可以使用`class`构造,也可以使用对象字面量的形式:
```js
class Counter {
constructor(limit) {
this.limit = limit
}
// 实现 Symbol.iterator 属性
[Symbol.iterator]() {
let count = 1, limit = this.limit
// 需要在该属性中返回一个迭代器
// 也即需要实现 next 方法
return {
// 实现 next 方法
next() {
if (count <= limit) {
// 返回 done 和 value 属性值
return { done: false, value: count++ }
} else {
return { done: true }
}
}
}
}
}
let counter1 = new Counter(5)
for (let i of counter1) {
console.log(i)
}
// 输出
// > 1
// > 2
// > 3
// > 4
// > 5
```
&emsp;&emsp;这里使用闭包(将`next()`方法放在了`return`语句中,并且在实例化时,仍然可以访问`count`变量)保存了`count`变量,使得可以同时创建多个迭代器。
```js
let makeRangeIterator = {
[Symbol.iterator]() {
start = 1, end = 10, step = 2
let nextIndex = start;
let iterationCount = 0;
const rangeIterator = {
next: function() {
let result;
if (nextIndex < end) {
result = { value: nextIndex, done: false }
nextIndex += step;
iterationCount++;
return result;
}
return { value: iterationCount, done: true }
}
};
return rangeIterator;
}
}
for (let it of makeRangeIterator) {
console.log(it)
}
// 输出
// 1
// 3
// 5
// 7
// 9
```
## 生成器
### 生成器概念
&emsp;&emsp;在函数名称前面加上星号`(*)`表示它是一个生成器(箭头函数不能用来定义生成器函数),调用生成器函数会产生一个生成器对象。
&emsp;&emsp;**生成器对象内部实现了`Iterator`接口,一开始处于暂停执行状态,调用`next()`方法可以让生成器开始或恢复执行。**
&emsp;&emsp;`next()`方法中的`value`属性是生成器函数的返回值,生成器函数只会在初次调用`next()`方法后开始执行。
&emsp;&emsp;使用生成器可以快速的构造迭代器,只需要在内部编写逻辑即可:
&emsp;&emsp;例如将之前的示例改为使用生成器构造:
```js
function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
let iterator = makeRangeIterator(1, 10, 2)
// 可以使用迭代器API
console.log(iterator.next())
// 可以使用可迭代对象的特性
for (let it of iterator) {
console.log(it)
}
// 输出
// {value: 1, done: false}
// 3
// 5
// 7
// 9
```
### yield关键字
&emsp;&emsp;`yield`关键字可以让生成器停止和开始执行。**生成器函数在遇到yield关键字之前会正常执行遇到之后会停止函数作用域会保留调用`next()`后会恢复执行。**通过`yiel`关键字退出的生成器函数会处在`done:false`状态,通过`return`关键字退出的生成器会处在`done:true`状态。
&emsp;&emsp;`yield`关键字必须只能位于生成器函数中定义。
```js
function* generatorFn() {
yield 'foo'
yield 'bar'
return 'baz'
}
let generatorObject = generatorFn()
console.log(generatorObject.next())
console.log(generatorObject.next())
console.log(generatorObject.next())
// 输出
// > {"value":"foo","done":false}
// > {"value":"bar","done":false}
// > {"value":"baz","done":true}
```
### 生成器应用
```js
// 生成器对象作为可迭代对象
function* generatorFn1() {
yield 1
yield 2
yield 3
}
// 使用星号增强yield
function* generatorFn2() {
yield* [1, 2, 3]
}
// 生成器作为默认迭代器
class Foo {
constructor() {
this.values = [1, 2, 3]
}
// 定义为生成器函数
*[Symbol.iterator] () {
yield* this.values
}
}
for (const x of generatorFn1()) {
console.log(x)
}
for (const x of generatorFn2()) {
console.log(x)
}
const f = new Foo()
for (const x of f) {
console.log(x)
}
// 输出值均相同
// > 1
// > 2
// > 3
```
## 总结
&emsp;&emsp;实现`[Symbol.iterator]`属性的对象就是可迭代对象,可迭代对象就可以使用`for-of`等原生的语言特性,实现`next()`方法的就是迭代器,就可以使用相应的`API`。在函数名称前加上`*`就是生成器,生成器内部实现了迭代器的接口,因此也可以使用迭代器的`API`,但同时也具有`yield`等特殊特性。
&emsp;&emsp;创建迭代器的方法有以下几种:
1. 使用原生具体可迭代对象性质的数据类型,如:
```js
const str = 'abc'
const strIterator = str[Symbol.iterator]()
console.log(strIterator.next())
```
2. 自定义具有`[Symbol.iterator]`属性和`next()`方法的对象/函数。
3. 使用生成器
&emsp;&emsp;本质上相对于可以自定义数据类型,创建具有类似于数组功能的数据结构,使用更改灵活。
## 参考资料
1. [ES6中的迭代器(Iterator)和生成器(Generator)](https://www.cnblogs.com/xiaohuochai/p/7253466.html)
# 对象、类
&emsp;&emsp;对象为一组属性的无序集合,可以将其想象为一张散列表,其中内容就是一组名/值对,值可以是**数据或者函数**。
## 对象属性
&emsp;&emsp;为对象的数据添加一些额外的属性效果,可以使用`Object.defineProperty(Object, keyName, options)`定义,也可以同时定义多个(使用`Object.definePropertys(Object, options)`)。
&emsp;&emsp;使用`Object.getOwnPropertyDescriptor(Object, keyName)`可以查看指定属性的描述符,也可以使用`Object.getOwnPropertyDescriptors(Object)`查看所有的属性描述符。
```js
// 定义对象,包含一个直接定义在对象上的属性
let book = {year_: 2017}
// 添加对象属性
Object.defineProperties(book, {
edition: {
// 数据属性
value: 1
},
year: {
// 访问器属性
// 获取函数 在读取属性时调用
get() {
return this.year_
},
// 设置函数 在写入属性时调用
set(newValue) {
if (newValue > 2017) {
this.year_ = newValue
}
}
}
})
// 打印对象属性信息
console.log(Object.getOwnPropertyDescriptors(book))
const descriptor = Object.getOwnPropertyDescriptor(book, "year")
// 打印访问器属性,注意这里是属性,不能使用函数调用()
console.log(typeof descriptor.get)
```
&emsp;&emsp;输出结果:
```js
{
"year_": {
// 直接定义在对象上的属性特性默认为true
// 属性实际值
"value": 2017,
// 属性的值是否可以被修改
"writable": true,
// 属性的值是否可以被枚举,即可以通过 for-in 返回
"enumerable": true,
// 属性的值是否可以被配置
"configurable": true
},
"edition": {
"value": 1,
"writable": false,
"enumerable": false,
"configurable": false
},
"year": {
"enumerable": false,
"configurable": false
}
}
"function"
```
## ES6增强的对象语法
```js
let name = 'cxx'
const ageKey = 'age'
let person = {
// 属性值简写 相当于 name:name
name,
// 可计算属性 []表示对象的属性键将其作为表达式运算
[ageKey]: 18,
job: 'engineer',
// 方法名简写 相当于 sayName: function(){}
sayName() {
console.log(name)
},
get name() {
return `My name is ${name}`
}
}
console.log(person.name)
person.sayName()
// 对象解构
let { job } = person
console.log(job)
// 输出
> My name is cxx
> cxx
> engineer
```
## 创建对象
### Object构造函数
```js
let person = new Object()
person.name = 'Cxx'
person.age = 18
person.job = 'Software Engineer'
person.sayName = function() {
console.log(this.name)
}
```
### 对象字面量
```js
let person = {
name: 'Cxx',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
}
}
```
&emsp;&emsp;以上两种方式创建对象比较方便,但是无法创建具有同样接口的多个对象。
### 工厂模式
```js
function createPerson(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function() {
console.log(this.name)
}
return o
}
let person1 = createPerson('cxx1', 18, "Student")
let person2 = createPerson('cxx2', 17, "Student")
```
&emsp;&emsp;可以解决创建多个类似对象的问题,但是无法确定新创建对象的类型。
### 构造函数模式
```js
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function() {
console.log(this.name)
}
}
let person1 = new Person('cxx1', 18, "Student")
let person2 = new Person('cxx2', 17, "Student")
console.log(person1.constructor)
console.log(person1.__proto__)
console.log(person1.prototype)
console.log('----------------')
console.log(Person.constructor)
console.log(Person.__proto__)
console.log(Person.prototype)
// 输出
> function Person(name, age, job) { this.name = name this.age = age this.job = job this.sayName = function() { console.log(this.name) } }
> {}
> undefined
> ----------------
> function Function() { [native code] }
> function () { [native code] }
> {}
```
&emsp;&emsp;这里使用了构造函数(`Person()`)来代替之前的工厂函数。
&emsp;&emsp;同时使用`new`操作符来创建`Person`的实例,以这种方式创建时,会执行以下步骤:
1. 在内存重创建一个新实例对象。(即`person1``person2`)
2. 在新实例对象内部的`[[Prototype]]`(也即`__proto__`属性)特性被赋值为构造函数的`prototype`属性。(即函数的`prototype`属性等于实例对象的`__proto__`属性:`Person.prototype === person1.__proto__`)
3. 构造函数内部的`this`被赋值为这个新的实例对象。(`this`指向新对象,即运行时赋值,`this.name`即新实例对象的`name`)
4. 执行构造函数内部的代码。(给新的实例对象添加属性和方法)
5. 如果构造函数返回非空对象,则返回改对象,否则,返回刚创建的对象。
&emsp;&emsp;这里需要记住2点`__proto__``constructor`属性是对象所独有的;②`prototype`属性是函数所独有的,因为函数也是一种对象,所以函数也拥有`__proto__``constructor`属性。
&emsp;&emsp;`constructor`属性是用来标识对象类型的,始终指向创建当前对象的构造函数。
&emsp;&emsp;任何函数只要使用`new`操作符调用就是构造函数。
&emsp;&emsp;然而使用构造函数创建对象仍然存在问题,因为在`JavaScript`中,函数也是对象。因此在创建`sayName()`函数时,本质上相当于:
```js
function Person(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = new Function(console.log(this.name))
}
let person1 = new Person('cxx1', 18, "Student")
let person2 = new Person('cxx2', 17, "Student")
console.log(person1.sayName === person2.sayName)
// 输出
> false
```
&emsp;&emsp;因为此时每个`Person`实例都会有自己的`Function`实例所以这2个同名函数实际上并不相等。
### 原型模式
&emsp;&emsp;每个函数都会创建一个`prototype`属性,该属性是一个对象,包含应该由特定引用类型的实例共享的属性和方法,这个对象也是通过调用构造函数创建的对象的原型。
```js
function Person() {}
Person.prototype.name = 'Cxx'
Person.prototype.age = 18
Person.prototype.job = 'Software Engineer'
Person.prototype.sayName = function() {
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
console.log(person1.sayName === person2.sayName)
// 输出
> true
```
&emsp;&emsp;所有的属性和方法都直接添加到`Person``prototype`属性上了,使用这种原型模式定义的属性和方法是由所有的实例对象共享的。
#### 深入理解原型
![理解原型](./images/理解原型.png)
&emsp;&emsp;`JavaScript`中,只要创建了一个函数,就会按照特定的规则**为这个函数创建一个`prototype`属性**(指向原型对象)。所有的**原型对象自动获得一个名为`constructor`的属性,指回与之关联的构造函数**。
&emsp;&emsp;在自定义构造函数时,原型对象默认只会获得`constructor`属性,其他的所有方法都继承自`Object`(本例中的其他方法是在后面自定义的)。每次调用构造函数会创建一个新实例对象,这个实例对象的内部`[[Prototype]]`指针就会被赋值为构造函数的原型对象(大部分浏览器将这个指针定义为实例对象上的`__proto__`属性)。通过这个属性就可以访问对象的原型。
&emsp;&emsp;因此,实例对象通过`__proto__`可以链接到原型对象,它实际上指向隐藏特性`[[Prototype]]`,构造函数通过`Prototype`属性链接到原型对象。**实例对象与构造函数原型之间有直接的联系,但实例对象与构造函数之间没有关系。**
&emsp;&emsp;在通过对象访问属性时,**如果没有找到这个属性,则搜索会沿着指针进入原型对象。**只要给对象实例添加一个属性,这个属性就会遮蔽原型对象上的同名属性。
&emsp;&emsp;只要通过对象可以访问属性或方法,`in`操作符就返回`true`(例`'name' in person1`),而`hasOwnProperty()`只有属性存在于实例上才返回`true`
&emsp;&emsp;附:参考资料:
1. [帮你彻底搞懂JS中的prototype、__proto__与constructor图解](https://chen-cong.blog.csdn.net/article/details/81211729?spm=1001.2101.3001.6650.1&utm_medium=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&depth_1-utm_source=distribute.pc_relevant.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1.pc_relevant_antiscanv2&utm_relevant_index=2)
2. [JS中prototype介绍](https://blog.csdn.net/qq_42497250/article/details/92845285)
#### 对象迭代
&emsp;&emsp;`Object.values()`返回对象值的数组,`Object.entries()`返回键值对的数组。
#### 注意事项
```js
function Person() {}
Person.prototype = {
name: 'Cxx',
age: 18,
job: 'Software Engineer',
sayName() {
console.log(this.name)
}
}
// 以字面量形式定义原型对象时,需要恢复 constructor 属性
// 且将其设置为不可枚举
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
})
let friend = new Person()
// 可以在原型对象上添加属性和方法
// 但不推荐在原生的原型对象上添加
// 可以创建一个自定义类 使其继承原生类型
Person.prototype.sayHi = function() {
console.log('hi')
}
friend.sayHi()
// 输出
// > hi
// 重写整个原型会切断最初原型与构造函数的联系
// 但实例对象引用的仍然是最初的原型
// 因此下面的`sayHello()`调用会报错
Person.prototype = {
sayHello() {
console.log('hello')
}
}
friend.sayHello()
// 输出
// > Uncaught TypeError: friend.sayHello is not a function
```
&emsp;&emsp;通过原型模式创建对象也存在问题:
1. 弱化了向构造函数传递初始化参数的能力
2. 所有实例对象可以共享原型对象的属性和方法
## 继承
&emsp;&emsp;构造函数、原型对象和实例对象之间的关系:**每个构造函数都有一个原型对象,原型对象有一个属性指回构造函数,而实例有一个内部指针指向原型对象。**
&emsp;&emsp;原型链:通过将原型对象赋值为另一个类型的实例。
```js
function SuperType() {
this.property = true
}
SuperType.prototype.getSuperValue = function() {
return this.property
}
function SubType() {
this.subproperty = false
}
// 继承 SuperType
// 将原型对象赋值为另一个类型的实例
SubType.prototype = new SuperType()
SubType.prototype.getSubValue = function() {
return this.subproperty
}
let instance = new SubType()
// 访问继承的方法
console.log(instance.getSuperValue())
```
&emsp;&emsp;通过继承,`SubType`的实例不仅能从`SuperType`的实例中继承属性和方法,而且还与`SuperType`的原型挂钩,同时也可以扩展原型搜索机制,搜索原型的原型。
&emsp;&emsp;所有的引用类型都继承自`Object`,任何函数的默认原型都是一个`Object`的实例对象。
&emsp;&emsp;子类需要覆盖或者添加父类的方法时,必须在原型赋值之后再添加到原型上。
&emsp;&emsp;**以对象字面量形式创建原型方法会破坏之前的原型链,相当于重写了原型链。**
## 类
&emsp;&emsp;类是`ES6`新的基础性语法糖结构,本质上仍是原型和构造函数的概念。
### 基础知识
```js
// 表达式声明
const Animal = class {}
// 类就是一种特殊函数
console.log(typeof Animal)
console.log('----------')
// 输出
// > function
// 类声明
class Person1 {
constructor(name) {
console.log(arguments.length)
}
}
// 如果不需要参数,类名后面的括号可以省略
let p1 = new Person1
console.log(p1.name)
console.log('----------')
// 输出
// > 0
// > undefined
class Person2 {
constructor(override) {
this.foo = 'foo'
if (override) {
// 默认返回 this 对象
// 也可以返回其他对象
return {
bar: 'bar'
}
}
}
}
let p2 = new Person2(true)
console.log(p2)
console.log('----------')
// 输出
// > {"bar":"bar"}
class Person3 {
constructor() {
// 添加到 this 的所有内容都会存在于不同的实例上
this.locate = () => {
console.log('instance')
}
}
// 定义在类的原型对象上
locate() {
console.log('prototype')
}
// 定义在类本身上
static locate() {
console.log('class')
}
}
let p3 = new Person3()
p3.locate()
Person3.prototype.locate()
Person3.locate()
// 输出
// > instance
// > prototype
// > class
```
&emsp;&emsp;类可以包括构造函数方法、实例方法、获取函数、设置函数和静态类方法。
&emsp;&emsp;`constructor`关键字用于在类定义块内部创建类的构造函数,在使用`new`操作符创建类的实例时,会调用该函数。
&emsp;&emsp;`JavaScript`中,类就是一种特殊的函数,因此类也有`prototype`属性,该原型对象也有一个`constructor`属性指向类本身。
&emsp;&emsp;类是`JavaScript`的一等公民,也可以像其他对象或函数引用一样把类作为参数传递。
&emsp;&emsp;每个实例都对应一个唯一的成员对象,所有成员都不会在原型上共享。
### 继承
&emsp;&emsp;`ES6`类支持单继承,使用`extends`关键字。类和原型上定义的方法都会被带到派生类。
#### Super关键字
```js
class Vehicle {
constructor() {
this.hasEngine = true
}
print() {
console.log(this.hasEngine)
}
static identify() {
console.log('vehicle')
}
}
// extends 继承
class Bus extends Vehicle {
constructor() {
// 调用父类构造函数
// super 作为函数调用
super()
// 调用父类原型对象上的方法
// super 作为对象调用
super.print()
}
static identify() {
// 调用父类的静态方法 指向父类
super.identify()
}
}
const bus = new Bus()
Bus.identify()
// 输出
// > true
// > vehicle
```
&emsp;&emsp;`super`既可以当函数使用,也可以当对象使用。**子类的构造函数必须执行一次`super`函数。**
&emsp;&emsp;不能单独引用`super`关键字,不能在调用`super()`之前引用`this`
# 函数
&emsp;&emsp;函数实际上也是对象,每个函数都是`Function`类型(属于引用类型)的实例。`Function`也有属性和方法。
&emsp;&emsp;`JavaScript`引擎在任何代码执行前,会先读取函数的声明。
## 箭头函数
&emsp;&emsp;箭头函数不能使用`arguments``super``new.target`,也不能用作构造函数,箭头函数也没有`prototype`属性。
## 函数名
&emsp;&emsp;函数名就是指向函数的指针,一个函数可以拥有多个名称。使用不带括号的函数名会访问函数指针,而不会执行函数。
&emsp;&emsp;`ES6`中所有的函数对象都会暴露一个只读的`name`属性,包含函数的信息。
## 函数参数
```js
// 扩展运算符收集参数
function doAdd(num = 5, ...nums) {
if (arguments.length === 1) {
console.log(num)
} else if (arguments.length === 2) {
// 此时 num 默认值会被覆盖
console.log(num)
console.log(arguments[0])
// 其他参数被收集到 nums 中
console.log(nums)
}
}
let values = [1, 2]
// 扩展运算符传值
doAdd(...values)
// 输出
// > 1
// > 1
// > [2]
```
&emsp;&emsp;`JavaScript`中,函数参数只是为了方便写出来,并不是必须写出来,并没有验证命名参数的机制,因此传递任意数量的参数都是可以的。
&emsp;&emsp;`arguments`对象是一个类数组对象,存放着函数的参数信息。`ES6`中可以显示定义默认参数。函数的默认参数只有在函数调用时才会求值,不会在函数定义时求值。
&emsp;&emsp;使用扩展运算符在收集参数时,只能把它作为最后一个参数。
## 函数内部
### this
&emsp;&emsp;在标准函数中,`this`引用的是把函数当成方法调用的上下文对象,在箭头函数中,`this`引用的是定义箭头函数的上下文。
```js
function King () {
this.royaltyName = 'Henry',
console.log(this.royaltyName)
setTimeout(() => {
console.log(this.royaltyName)
}, 1000)
setTimeout(function() {
console.log(this.royaltyName)
}, 1000)
}
new King()
// 输出
// > Henry
// > Henry
// > undefined
```
&emsp;&emsp;在利用函数创建类时,如果使用普通函数,则无法访问到内部的`this`值(此时作用域位于`Window`)。
## 函数方法
&emsp;&emsp;函数除了可以使用函数名+`()`的形式调用,也可以使用`apply()``call()`方法调用这2个方法的第一个参数均可以指定函数内部的`this`,调用`apply()`方法时,传入的参数需要为一个数组,而调用`call()`方法时,需要将参数一一列出。
```js
window.color = 'red'
let o = {
color: 'blue'
}
function sayColor(a, b) {
console.log(a, b)
console.log(this.color)
}
// 正常调用的 this 为 Window对象
sayColor(1, 2)
console.log('**********')
// 第2个参数为数组形式
sayColor.apply(window, [1, 2])
sayColor.apply(o, [1, 2])
console.log('**********')
// 需要将参数一一列出
sayColor.call(window, 1, 2)
sayColor.call(o, 1, 2)
// 输出
// > 1,2
// > red
// > **********
// > 1,2
// > red
// > 1,2
// > blue
// > **********
// > 1,2
// > red
// > 1,2
// > blue
```
## 函数作为值
&emsp;&emsp;可以把函数作为参数传给另一个函数,也可以在一个函数中返回另一个函数。(实际上就是把函数名当指针使用)
## 闭包
&emsp;&emsp;闭包指的是**引用了另一个函数作用域变量的函数**,通常是在嵌套函数中实现的。
```js
// 正常函数
function compare(value1, value2) {
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
let result = compare(5, 10)
```
![普通函数作用域链](./images/普通函数作用域链.png)
&emsp;&emsp;如图所示,`compare()`函数是在全局上下文中调用的,第一次调用时,会为它创建一个包含`arguments``value1``value2`的活动对象,该对象是其作用域链上的第一个对象,而全局上下文的变量对象则是`compare()`作用域链上的第二个对象,包含`this``result``compare`
&emsp;&emsp;全局上下文会在代码执行期间始终存在,而函数局部上下文只在函数执行期间存在。
&emsp;&emsp;在定义`compare()`函数时,会为其创建作用域链,预装载全局变量对象,并保存在内部的`[[Scope]]`中。在调用这个函数时,会创建相应的执行上下文,然后通过复制函数的`[[Scope]]`来创建其作用域链,接着会创建函数的活动对象并将其推入作用域链的前端。
&emsp;&emsp;函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中只剩下全局作用域。
&emsp;&emsp;然而,闭包中就不一样了。
```js
// 闭包函数
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName]
let value2 = object2[propertyName]
if (value1 < value2) {
return -1
} else if (value1 > value2) {
return 1
} else {
return 0
}
}
}
let compare = createComparisonFunction('name')
let result = compare({'name': 'Cxx'}, {'name': 'Pjm'})
```
![闭包函数作用域链](./images/闭包函数作用域链.png)
&emsp;&emsp;**在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。**
&emsp;&emsp;`createComparisonFunction()`返回匿名函数后,其作用域链会被初始化为包含`createComparisonFunction()`的活动对象和全局变量对象,这样匿名函数就可以访问到`createComparisonFunction()`可以访问到的所有变量。
&emsp;&emsp;而当`createComparisonFunction()`执行完毕后,匿名函数仍然有它的引用,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。
### 闭包的作用
&emsp;&emsp;**闭包常常用来间接访问一个变量,或者隐藏一个变量**。(使用全局变量也可以满足,但是会导致变量变为公有,不能起到一定的保护作用。)
### 附:关于闭包的形式解释
&emsp;&emsp;这里有一个小故事,可以更好的理解函数闭包的含义:[公主的故事](https://stackoverflow.com/questions/111102/how-do-javascript-closures-work)
&emsp;&emsp;有一个公主...
```js
function princess() {
```
&emsp;&emsp;她生活在一个充满冒险的奇妙世界。她遇到了她的白马王子,骑着独角兽环游世界,与龙搏斗,遇到会说话的动物,以及许多其他奇幻事物。
```js
var adventures = [];
function princeCharming() { /* ... */ }
var unicorn = { /* ... */ },
dragons = [ /* ... */ ],
squirrel = "Hello!";
/* ... */
```
&emsp;&emsp;但她总是不得不回到她枯燥的家务和成年人的世界。
```js
return {
```
&emsp;&emsp;她经常告诉他们她作为公主的最新惊人冒险。
```js
story: function() {
return adventures[adventures.length - 1];
}
};
}
```
&emsp;&emsp;但他们看到的只是一个小女孩……
```js
var littleGirl = princess();
```
&emsp;&emsp;...讲述关于魔法和幻想的故事。
```js
littleGirl.story();
```
&emsp;&emsp;即使大人们知道真正的公主,他们也永远不会相信独角兽或龙,因为他们永远看不到它们。大人们说,他们只存在于小女孩的想象中。
&emsp;&emsp;但我们知道真相;那个带着公主的小女孩……
&emsp;&emsp;……真是个公主,里面有一个小女孩。
&emsp;&emsp;`Princess()`函数是一个包含私有数据的复杂作用域。在函数之外,不能看到或访问私有数据。公主把独角兽、龙、冒险等都留在了她的想象中(私人数据),大人自己看不到。但是公主的想象力被`story()`函数的闭包捕捉到了,这是`littleGirl`实例暴露给魔法世界的唯一接口。
### 参考资料
1. [闭包MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures)
2. [JS 中的闭包是什么?](https://zhuanlan.zhihu.com/p/22486908)
3. [关于闭包](https://cnodejs.org/topic/5d39c5259969a529571d73a8)
# 代理与反射
&emsp;&emsp;代理用于定义基本操作的自定义行为,本质属于元编程(`meta programming`),可以修改程序的默认形为,就形同于在编程语言层面上做修改。
&emsp;&emsp;元编程,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。
&emsp;&emsp;代理是目标对象的抽象,可以用作目标对象的替身,但又完全独立于目标对象。目标对象既可以直接被操作,也可以通过代理来操作。
## 代理基础
```js
const target = {
foo: 'bar'
}
const handler = {
// 捕获器键值为函数
// 接收三个参数 目标对象 要查询的属性 代理对象
get: function(trapTarget, property, receiver) {
console.log('trapTarget:', trapTarget)
console.log('property:', property)
console.log('receiver:', receiver)
return trapTarget[property] + ' get handler'
},
// ES6 对象方法简写
set() {
console.log('set handler')
// 反射API
Reflect.set
}
}
// 接收两个参数 目标对象 处理程序对象
// 注:这里是对象 不是方法
const proxy = new Proxy(target, handler)
console.log(target.foo)
target.foo = 'foo'
console.log('**********')
// 需要对 Proxy 对象进行操作 才会触发代理
console.log(proxy.foo)
proxy.foo = 'foo'
// 输出
// bar
// **********
// trapTarget: {foo: "foo"}
// property: foo
// receiver: Proxy {foo: "foo"}
// foo get handler
// set handler
```
* 代理使用`Proxy`构造函数创建,该函数接收两个参数:**目标对象和处理程序对象。**
* 在代理对象上执行的任何操作实际上都会应用到目标对象,**每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。**
* 只有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行这些操作仍然会产生正常的行为。
| 捕获器 API | Object API |
| :-----:| :----: |
| handler.getPrototypeOf() | Object.getPrototypeOf() |
| handler.setPrototypeOf() | Object.setPrototypeOf() |
| handler.isExtensible() | Object.isExtensible() |
| handler.preventExtensions() | Object.preventExtensions() |
| handler.getOwnPropertyDescriptor() | Object.getOwnPropertyDescriptor() |
| handler.defineProperty() | Object.defineProperty() |
| handler.has() | in 操作符 |
| handler.get() | 属性读取操作 |
| handler.set() | 属性设置操作 |
| handler.deleteProperty() | delete 操作符 |
| handler.ownKeys() | Object.getOwnPropertyNames()和Object.getOwnPropertySymbols() |
| handler.apply() | 函数调用操作 |
| handler.construct() | new 操作符 |
&emsp;&emsp;[handler 对象的方法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
```js
const target = {
foo: 'bar'
}
const handler = {
get(trapTarget, property, receiver) {
// 通过反射API简化代码
return Reflect.get(...arguments)
}
}
// 对象解构
const { proxy, revoke } = Proxy.revocable(target, handler)
console.log(proxy.foo)
// 撤销代理
revoke()
console.log(proxy.foo)
// 输出
// bar
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
```
&emsp;&emsp;处理程序对象中所有可以捕获的方法都有对应的反射`API`方法,可以通过调用反射`API`实现简化代码。
| Reflect API | Object API | 功能/作用 |
| :-----:| :----: | :----: |
| Reflect.apply(target, thisArgument, argumentsList) | Function.prototype.apply() | 对一个函数进行调用操作,同时可以传入一个数组作为调用参数 |
| Reflect.construct(target, argumentsList[, newTarget]) | new target(...args) | 对构造函数进行 new 操作 |
| Reflect.defineProperty(target, propertyKey, attributes) | Object.defineProperty() | 如果设置成功就会返回 true |
| Reflect.deleteProperty(target, propertyKey) | delete target[name] | 作为函数的delete操作符 |
| Reflect.get(target, propertyKey[, receiver]) | target[name] | 获取对象身上某个属性的值 |
| Reflect.getOwnPropertyDescriptor(target, propertyKey) | Object.getOwnPropertyDescriptor() | 如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined |
| Reflect.getPrototypeOf(target) | Object.getOwnPropertyDescriptor() | |
| Reflect.has(target, propertyKey) | in 运算符 | 判断一个对象是否存在某个属性 |
| Reflect.isExtensible(target) | Object.isExtensible() | |
| Reflect.ownKeys(target) | Object.keys() | 返回一个包含所有自身属性(不包含继承属性)的数组 |
| Reflect.preventExtensions(target) | Object.preventExtensions() | 返回一个Boolean |
| Reflect.set(target, propertyKey, value[, receiver]) | | 将值分配给属性的函数。返回一个Boolean如果更新成功则返回true |
| Reflect.setPrototypeOf(target, prototype) | | 设置对象原型的函数. 返回一个 Boolean 如果更新成功则返回true |
&emsp;&emsp;[Reflect静态方法](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect)
## 典型应用
```js
const user = {
name: 'Cxx',
age: 18
}
const proxy = new Proxy(user, {
get(target, property, receiver) {
// 跟踪属性访问
console.log(`get ${property}`)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
// 跟踪属性访问
console.log(`set ${property}`)
return Reflect.set(...arguments)
}
})
proxy.name
proxy.age = 19
console.log('**********')
const hiddenProperties = ['name']
const proxy2 = new Proxy(user, {
get(target, property, receiver) {
// 隐藏属性
if (hiddenProperties.includes(property)) {
return undefined
} else {
return Reflect.get(...arguments)
}
},
has(target, property) {
// 隐藏属性
if (hiddenProperties.includes(property)) {
return undefined
} else {
return Reflect.has(...arguments)
}
}
})
console.log(proxy2.name)
console.log('name' in proxy2)
console.log('**********')
const proxy3 = new Proxy(user, {
set(target, property, value, receiver) {
// 属性验证
if (property === 'age' && typeof value !== 'number') {
return false
} else {
return Reflect.set(...arguments)
}
}
})
proxy3.age = '20'
// 更改无效
console.log(proxy3.age)
proxy3.age = 20
console.log(proxy3.age)
console.log('**********')
// 输出
// get name
// set age
// **********
// undefined
// false
// **********
// 19
// 20
// **********
```
# Promise与异步函数
&emsp;&emsp;`JavaScript`是一种单线程时间循环模型。
## Promise基础
&emsp;&emsp;`Promise`(期约或者承诺)是`ES6`新增的引用类型,可以通过`new`操作符进行实例化。创建`Promise`时需要传入执行器函数作为参数该函数接收2个可以改变`Promise`状态的函数,一般命名为`resolve``reject`
```js
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor')
resolve('p1')
})
let p2 = new Promise((resolve, reject) => {
setTimeout(reject, 1000, 'p2 executor')
})
let p3 = new Promise(() => {
})
// p1 的状态为 fulfilled
console.log('p1:', p1)
// 此时 p2 的状态为 pending
console.log('p2:', p2)
// p3 的状态为 pending
console.log('p3:', p3)
setTimeout(() => {
// p1 的状态为 fulfilled
console.log('p1:', p1)
// 此时 p2 的状态为 reject
console.log('p2:', p2)
// p3 的状态为 pending
console.log('p3:', p3)
}, 1000);
```
&emsp;&emsp;`Promise`一共有3种状态待定`pending`、兑现`fulfilled`(或者称为解决`resolved`)和拒绝`rejected`。无论何时,`Promise`有且只可能是其中一种状态,且不可逆。待定是`Promise`的最初始状态,也是默认状态,当在`Promise`内部调用控制`Promise`状态的函数时,则会进行相应的状态切换,否则一致保持待定状态。
![promise基础](./images/promise基础.png)
&emsp;&emsp;`p1`中,立即调用`resolve()`函数将其状态转为解决,而在`p2`通过延迟1秒再将其状态转为拒绝`p3`会一直保持默认状态。
&emsp;&emsp;因此,`p1`的状态会一直保持解决状态,并带有解决的返回值。`p2`的状态一开始为待定1秒后会变成拒绝状态也带有拒绝的返回值并且控制台会提示错误信息。`p3`则会一直处于待定状态,并没有返回值。
## Promise实例方法
```js
let p1 = new Promise((resolve, reject) => {
console.log('p1 executor')
setTimeout(resolve, 1000)
})
// then 只会在 Promise 进入 解决或者拒绝 状态时执行
p1.then(() => new Promise((resolve, reject) => {
console.log('p2 executor')
setTimeout(resolve, 1000, 'p2 value')
})).catch(() => {
console.log('p1 catch')
}).finally(() => {
console.log('p1 finally')
// then 仍然返回 Promise 可以继续链式调用
// 将解决/拒绝的值作为参数进行向下传递
}).then((value) => new Promise((resolve, reject) => {
console.log(value)
console.log('p3 executor')
setTimeout(reject, 1000)
// 也可以将 catch 写在 then 中 即作为第二个参数
})).then(() => {
console.log('p3 resolve')
}, () => {
console.log('p3 reject')
})
// 输出
// p1 executor
// p2 executor
// p1 finally
// p2 value
// p3 executor
// p3 reject
```
&emsp;&emsp;`then()`实例方法接收2个参数第一个为处理解决状态的函数第二个为处理拒绝状态的函数并且可以将解决/拒绝状态的返回值作为函数的参数进行传递。`Promise`状态只能为其中一个,因此也只能进入其中一个函数。
&emsp;&emsp;`catch()`实例方法实际为`then()`方法的语法糖,表示处理拒绝状态的函数。`finally()`实例方法表示无论转换为解决还是拒绝状态都是执行的函数。
&emsp;&emsp;如果`Promise`状态一直为待定状态,则不会进入这些实例方法。
## Promise连锁
```js
let p = Promise.all([
// p1 也可以直接调用实例方法创建 Promise
Promise.resolve('p1 executor'),
// p2
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2 executor')
}, 1000);
}),
// p3
Promise.reject('p3 executor')
])
// 此时 p 的状态为 pending
console.log(p)
// 此时 p 的状态为 rejected
setTimeout(console.log, 1000, p)
// 只会进入 catch 实例方法
p.then((resolve) => {
console.log(resolve)
}).catch((reject) => {
console.log(reject)
})
// 输出
// Promise {<pending>}
// p3 executor
// Promise {<rejected>: "p3 executor"}
```
&emsp;&emsp;`Promise.all()`接收一个可迭代对象,会依次处理每个`Promise`的状态。如果至少有一个`Promise`的状态为待定/拒绝,则合成的`Promise`的状态为待定/拒绝,且此时返回的拒绝值为第一个拒绝的值。如果所有的`Promise`的状态为解决,则合成的`Promise`的状态为解决,且返回的解决值为所有解决值的数组。
```js
let p = Promise.race([
// p1
Promise.resolve('p1 executor'),
// p2
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('p2 executor')
}, 1000);
}),
// p3
Promise.reject('p3 executor')
])
// 此时 p 的状态为 pending
console.log(p)
// 此时 p 的状态为 fulfilled
setTimeout(console.log, 1000, p)
// 只会进入 then 方法
p.then((resolve) => {
console.log(resolve)
}).catch((reject) => {
console.log(reject)
})
```
&emsp;&emsp;`Promise.race()``Promise.all()`类似,但只会处理最先解决/拒绝的`Promise`。只要有一个`Promise`已经解决/处理,则不会再进行后续操作。
## 异步函数
&emsp;&emsp;`async`关键字用于声明异步函数,异步函数始终返回期约对象,`await`关键字会暂停执行异步函数后面的代码,让出`JavaScript`运行时的执行线程,`await`关键字必须在异步函数中使用。
```js
async function foo() {
console.log('foo start')
// 碰到 await 会暂停执行 直到 await 右边的状态确定
const result = await Promise.resolve('p1')
console.log('result:', result)
console.log('foo end')
}
function bar() {
setTimeout(() => {
console.log('bar setTimeout')
}, 1000);
console.log('bar start')
}
foo()
bar()
// 输出
// foo start
// bar start
// result: p1
// foo end
// bar setTimeout
```
&emsp;&emsp;`JavaScript`运行时在碰到`await`关键字时,会记录在哪里暂停执行,等到`await`右边的值可用了,`JavaScript`运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
# 参考资料
1. JavaScript高级程序设计 第四版
2. [ES6标准入门 阮一峰](https://es6.ruanyifeng.com/)