迭代器可迭代对象
什么是迭代器?
◼ 迭代器(iterator),使用户在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
- 其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
- 在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
◼ 从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
◼ 在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;
- 在JavaScript中这个标准就是一个特定的next方法;
◼ next方法有如下的要求:
- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
- done(boolean)
- 如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)
- 如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- value
原生的迭代器对象
可迭代对象
◼ 但是上面的代码整体来说看起来是有点奇怪的:
- 我们获取一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
- 事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
◼ 什么又是可迭代对象呢?
- 它和迭代器是不同的概念;
- 当一个对象实现了iterable protocol协议时,它就是一个可迭代对象;
- 这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;
◼ 当然我们要问一个问题,我们转成这样的一个东西有什么好处呢?
- 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作;
比如 for…of 操作时,其实就会调用它的 @@iterator 方法;
可迭代对象的代码
原生迭代器对象
◼ 事实上我们平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:
String、Array、Map、Set、arguments对象、NodeList集合;
可迭代对象的应用
◼ 那么这些东西可以被用在哪里呢?
- JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment);
- 创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]);
- 一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
自定义类的迭代器
自定义类的迭代
◼ 在前面我们看到Array、Set、String、Map等类创建出来的对象都是可迭代对象:
- 在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:
- 如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上 @@iterator 方法;
◼ 案例:创建一个classroom的类
- 教室中有自己的位置、名称、当前教室的学生;
- 这个教室可以进来新学生(push);
- 创建的教室对象是可迭代对象;
自定义类的迭代实现
迭代器的中断
◼ 迭代器在某些情况下会在没有完全迭代的情况下中断:
- 比如遍历的过程中通过break、return、throw中断了循环操作;
- 比如在解构的时候,没有解构所有的值;
◼ 那么这个时候我们想要监听中断的话,可以添加return方法:
生成器理解和作用
什么是生成器?
◼ 生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。
- 平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
◼ 生成器函数也是一个函数,但是和普通的函数有一些区别:
- 首先,生成器函数需要在function的后面加一个符号:*
- 其次,生成器函数可以通过yield关键字来控制函数的执行流程:
最后,生成器函数的返回值是一个Generator(生成器):
那么我们如何可以让它执行函数中的东西呢?调用next即可;
- 我们之前学习迭代器时,知道迭代器的next是会有返回值的;
- 但是我们很多时候不希望next返回的是一个undefined,这个时候我们可以通过yield来返回结果;
生成器传递参数 – next函数
◼ 函数既然可以暂停来分段执行,那么函数应该是可以传递参数的,我们是否可以给每个分段来传递参数呢?
- 答案是可以的;
- 我们在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值;
- 注意:也就是说我们是为本次的函数代码块执行提供了一个值;
生成器提前结束 – return函数
◼ 还有一个可以给生成器函数传递参数的方法是通过return函数:
- return传值后这个生成器函数就会结束,之后调用next不会继续生成值了;
生成器抛出异常 – throw函数
◼ 除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:
- 抛出异常后我们可以在生成器函数中捕获异常;
- 但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行;
生成器替代迭代器
◼ 我们发现生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器:
◼ 事实上我们还可以使用yield*来生产一个可迭代对象:
- 这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
自定义生成器方案
自定义类迭代 – 生成器实现
对生成器的操作
◼ 既然生成器是一个迭代器,那么我们可以对其进行如下的操作:
异步处理方案解析
◼ 学完了我们前面的Promise、生成器等,我们目前来看一下异步代码的最终处理方案。
◼ 案例需求:
- 我们需要向服务器发送网络请求获取数据,一共需要发送三次请求;
- 第二次的请求url依赖于第一次的结果;
- 第三次的请求url依赖于第二次的结果;
- 依次类推;
Generator方案
◼ 但是上面的代码其实看起来也是阅读性比较差的,有没有办法可以继续来对上面的代码进行优化呢?
自动执行generator函数
◼ 目前我们的写法有两个问题:
- 第一,我们不能确定到底需要调用几层的Promise关系;
- 第二,如果还有其他需要这样执行的函数,我们应该如何操作呢?
◼ 所以,我们可以封装一个工具函数execGenerator自动执行生成器函数:
Day11 作业布置
三. 什么是迭代器?什么是可迭代对象?
迭代器
- 迭代器是帮助我们对某个数据结构进行遍历的对象
- 迭代器也是一个具体的对象,这个对象需要符合迭代器协议
- 迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式
- 在
JavaScript
中这个标准就是一个特定的next
方法
next
方法的要求- 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done
(boolean
)- 如果迭代器可以产生序列中的下一个值,则为
false
。(这等价于没有指定done
这个属性。) - 如果迭代器已将序列迭代完毕,则为
true
。这种情况下,value
是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
- 如果迭代器可以产生序列中的下一个值,则为
value
- 迭代器返回的任何
JavaScript
值。done
为true
时可省略// 封装一个为数组创建迭代器的函数
function createArrayIterator(arr) {
let index = 0
return {
next: function() {
if (index < arr.length) {
return { done: false, value: arr[index++] }
} else {
return { done: true }
}
}
}
}
- 迭代器返回的任何
可迭代对象
- 和迭代器不是一个概念
- 当一个对象实现了
iterable protocol
协议时,它就是一个可迭代对象; - 这个对象的要求是必须实现
@@iterator
方法,在代码中我们使用Symbol.iterator
访问该属性
- 当一个对象实现了
- 转成这样的好处
- 当一个对象变成一个可迭代对象的时候,就可以进行某些迭代操作
- 比如
for...of
操作时,其实就会调用它的@@iterator
方法
- 实现可迭代协议的原生对象
String
、Array
、Map
、Set
、arguments
对象、NodeList
集合…
- 可迭代对象的应用
- JavaScript中语法:
for ...of
、展开语法(spread syntax
)、yield*
、解构赋值(Destructuring_assignment
) - 创建一些对象时:
new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable])
- 一些方法的调用:
Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)
- JavaScript中语法:
- 迭代器的中断
- 比如遍历的过程中通过
break、return、throw
中断了循环操作 - 比如在解构的时候,没有解构所有的值
- 比如遍历的过程中通过
- 自定义类的迭代实现
class Person {
constructor(name, age, height, friends) {
this.name = name
...
}
// 实例方法
running() {}
/ [Symbol.iterator]() {
let index = 0
const iterator = {
next: () => {
if (index < this.friends.length) {
return { done: false, value: this.friends[index++] }
} else {
return { done: true }
}
}
}
return iterator
}
}
- 和迭代器不是一个概念
四. 什么是生成器?生成器和迭代器有什么关系?
生成器 (ES6新增)
- 生成器函数也是一个函数,但是和普通的函数有一些区别
- 首先,生成器函数需要在function的后面加一个符号:*
- 其次,生成器函数可以通过yield关键字来控制函数的执行流程:
- 最后,生成器函数的返回值是一个(生成器)
- 生成器事实上是一种特殊的迭代器
生成器函数
生成器函数:
1.function后面会跟上符号: *
2.代码的执行可以被yield控制
3.生成器函数默认在执行时, 返回一个生成器对象
* 要想执行函数内部的代码, 需要生成器对象, 调用它的next操作
* 当遇到yield时, 就会中断执行
生成器传递参数 — next函数
- 调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值
- 也就是说我们是为本次的函数代码块执行提供了一个值
- 生成器提前结束
- return传值后这个生成器函数就会结束,之后调用next不会继续生成值
- 生成器抛出异常 — throw函数
- 抛出异常后我们可以在生成器函数中捕获异常
- 但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行
生成器替代迭代器
- 使用yield来生产一个可迭代对象
- 这个时候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值
function* createArrayIterator(arr) {
yield* arr
}
自定义类迭代 — 生成器实现
// 以Person为例 添加到实例方法
*[Symbol.iterator]() {
yield* this.friends
}
- 生成器函数也是一个函数,但是和普通的函数有一些区别
一. 完成课堂所有的代码
二. 异步函数和普通函数的区别
- 异步函数
- 使用
async
关键字声明的函数 ```javascript // async function foo() {}
- 使用
// const bar = async function() {}
// const baz = async () => {}
// class Person { // async running() {} // }
- 异步函数的执行流程
- 异步函数的内部代码执行过程和普通的函数是一致的,默认情况下也是会被同步执行
- 返回值和普通函数的区别
- 情况一:异步函数也可以有返回值,但是异步函数的返回值相当于被包裹到`Promise.resolve`中
- 情况二:如果我们的异步函数的返回值是`Promise`,状态由会由`Promise`决定;
- 情况三:如果我们的异步函数的返回值是一个对象并且实现了`thenable`,那么会由对象的方`then`法来决定
- 如果在`async`函数中抛出异常
- 并不会报错, 而是作为`Promise`的`reject`来传递
<a name="f244f913"></a>
## 三. 说说线程和进程的区别以及关系
- **进程**
- 是`cpu`分配资源的最小单位;(是能拥有资源和独立运行的最小单位)
- 计算机已经运行的程序,是操作系统管理程序的一种方式 (**官方说法**)
- 可以认为启动一个应用程序,就会默认启动一个进程(也可能是多个进程)**(个人解释)**
- 也可以说进程是线程的容器
- **线程**
- 是`cpu`调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
- 操作系统能够运行运算调度的最小单位,通常情况下它被包含在进程中 **(官方说法)**
- 每一个进程中,都会启动至少一个线程用来执行程序中的代码,这个线程被称之为主线程
- **操作系统的工作方式**
- 如何做到同时让多个进程同时工作?
- 因为`CPU`的运算速度非常快, 可以快速的在多个进程之间迅速的切换
- 当进程中的线程获取到世间片时, 就可以快速执行我们编写的代码
- 由于`CPU`执行速度过于变态, 对于用户来说是感受不到这种快速切换的
- **浏览器中**`**JavaScript**`**线程**
- `**JavaScript**`**为什么是单线程的**
```javascript
// 这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
- 进程容器是浏览器或者
Node
- 浏览器是多进程的?
// 放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。所以,浏览器是一个多进程的。为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM.所以,这个新标准并没有改变JavaScript单线程的本质。
- 浏览器是多进程的?
- 目前多数浏览器都是多进程的, 打开一个
tab
页面就会开启一个新的进程, 防止因为一个页面的卡死导致浏览器的强制退出 - 每个进程中又有很多的线程,其中包括执行
JavaScript
代码的线程- 线程中
JavaScript
代码的执行
- 线程中
- 它是在一个单独的线程中执行的, 意味着
JavaScript
代码在同一时刻只能做一件事 - 这非常耗时, 意味着当前线程会被阻塞
- 所以耗时的操作并不是在
JavaScript
线程中执行的
- 所以耗时的操作并不是在
- 浏览器的每个进程是多线程的,那么其他线程可以来完成这个耗时的操作
- 比如网络请求、定时器,只需要在特性的时候执行应该有的回调即可
四. 说说你对事件队列、微任务、宏任务的理解
- 事件队列
- 事件队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点
- 宏/微任务
- 首先它们都是异步任务
- 宏任务列队
- 用来保存待执行的宏任务(回调)
- 如:
pajax、setTimeout、setInterval、DOM
监听、UI Rendering
等 会被加入到宏列队
- 微任务列队
- 用来保存待执行的微任务(回调)
- 如:
Promise
的then
回调、Mutation Observer API、queueMicrotask()
等 会被加入到微列队
JS
执行时会区别这两个队列JS
引擎首先必须先执行所有的初始化同步任务代码- 每次准备取出第一个宏任务执行前,都要将所有的微任务一个一个取出来执行,
- 也就是优先级比宏任务高,且与微任务所处的代码位置无关
- 也就是宏任务执行之前,必须保证微任务队列是空的;
- 如果不为空,那么就优先执行微任务队列中的任务(回调)