JavaScript 进阶笔记的第 5 篇:迭代器与生成器,以及 async-await、进程线程和事件循环。
这一块的顺序很舒服——先有迭代器,再有生成器,然后用生成器实现异步流程,最后自然过渡到 async/await,再揭开事件循环。
迭代器#
初识迭代器#
迭代器是帮助我们遍历某个数据结构的对象。JavaScript 的标准是它要实现一个特定的 next 方法:无参数或者一个参数,返回一个拥有以下两个属性的对象。
done(boolean):如果迭代器可以产生序列中的下一个值,则为false(等价于没有指定这个属性);如果把序列迭代完了,则为true。此时value是可选的,如果依然存在,就是迭代结束后的默认返回值。value:迭代器返回的任何 JavaScript 值,done为true时可以省略。
const names = ["abc", "cba", "nba"];
let index = 0;
const namesIterator = {
next: function () {
if (index < names.length) {
return { done: false, value: names[index++] };
} else {
return { done: true };
}
},
};
console.log(namesIterator.next()); // { done: false, value: 'abc' }
console.log(namesIterator.next()); // { done: false, value: 'cba' }
console.log(namesIterator.next()); // { done: false, value: 'nba' }
console.log(namesIterator.next()); // { 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 方法。
const infos = {
name: "why",
age: 18,
height: 1.88,
[Symbol.iterator]: function () {
const entries = Object.entries(this);
let index = 0;
const iterator = {
next: function () {
if (index < entries.length) {
return { done: false, value: entries[index++] };
} else {
return { done: true };
}
},
};
return iterator;
},
};很多原生对象已经实现了可迭代协议:String、Array、Map、Set、arguments 对象、NodeList 集合。
应用场景:
- 语法:
for...of、展开语法、yield*、解构赋值; - 创建对象:
new Map([iterable])、new Set([iterable])、WeakMap / WeakSet 同理; - 方法调用:
Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable)。
自定义类的迭代#
Array、Set、String、Map 创建出来的对象都是可迭代对象。想让自己的类创建出来的对象默认可迭代,只要在类里加上 @@iterator 方法:
class Person {
constructor(name, age, height, friends) {
this.name = name;
this.age = age;
this.height = height;
this.friends = friends;
}
[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;
}
}
const p1 = new Person("why", 18, 1.88, ["curry", "kobe", "james"]);
for (const item of p1) {
console.log(item);
}迭代器的中断#
遍历过程中通过 break、return、throw 中断循环,或者在解构时没有解构所有的值,都会中断迭代器。
想监听中断,可以在迭代器里添加 return 方法:
const iterator = {
next: () => {
// ...
},
return: () => {
console.log("监听到迭代器中断了");
return { done: true };
},
};生成器#
认识生成器#
生成器是 ES6 新增的一种函数控制方案,可以更灵活地控制函数什么时候继续执行、什么时候暂停。
生成器函数也是函数,但和普通函数有一些区别:
- 需要在
function后面加一个*; - 可以通过
yield关键字控制函数的执行流程; - 调用生成器函数,返回值是一个 Generator(生成器)。
生成器事实上是一种特殊的迭代器。
生成器的使用#
调用 next 可以让它继续执行,next 会执行到下一次遇到 yield 为止。
function* foo() {
console.log("1111");
console.log("2222");
yield;
console.log("3333");
console.log("4444");
yield;
console.log("5555");
console.log("6666");
}
const generator = foo();
generator.next(); // 1111 2222
generator.next(); // 3333 4444
generator.next(); // 5555 6666参数与返回值:
- 参数在
yield的前面(next传入的值会成为上一个yield表达式的返回值); - 返回值在
yield的后面({ value, done }里的 value); - 第一个参数在获得生成器时传入。
function* foo2(name1) {
console.log("执行内部代码:1111", name1);
const name2 = yield "aaaa"; // name2 是下一次 next 传入的参数,"aaaa" 是返回对象的 value
console.log("执行内部代码:3333", name2);
const name3 = yield "bbbb";
console.log("执行内部代码:5555", name3);
yield "cccc";
return undefined;
}
const generator2 = foo2("next1");
console.log(generator2.next()); // { value: 'aaaa', done: false }
console.log(generator2.next("next2")); // { value: 'bbbb', done: false }
console.log(generator2.next("next3")); // { value: 'cccc', done: false }生成器提前返回#
return:可以给生成器函数传参。传值之后生成器函数就会结束,之后调用 next 不会再生成值。
console.log(generator2.next());
console.log(generator2.return("next2")); // { value: 'next2', done: true }
console.log(generator2.next("next3")); // { value: undefined, done: true }throw:除了传参,还可以向生成器函数内部抛出异常,异常可以在生成器函数里被捕获。
- 抛出异常后可以在生成器函数中捕获;
- 但 catch 语句中不能继续
yield新值,可以在 catch 语句外继续yield中断函数执行。
console.log(generator3.next());
console.log(generator3.throw(new Error("next2 throw error")));生成器替代迭代器#
因为生成器本身就是特殊的迭代器,用 yield* 可以直接产出一个可迭代对象,写起来比自己维护 index 简单得多。
function* createArrayIterator(arr) {
yield* arr;
}类里同样可以:
class Person2 {
constructor(name, age, height, friends) {
this.name = name;
this.age = age;
this.height = height;
this.friends = friends;
}
*[Symbol.iterator]() {
yield* this.friends;
}
}异步 async-await#
网络请求异步处理#
需求:向服务器连发三次请求,第二次的 url 依赖第一次的结果,第三次依赖第二次,依次类推。
先把请求封装成一个返回 Promise 的方法:
function requestData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(url);
}, 2000);
});
}方式一:层层嵌套(回调地狱 callback hell)
function getData1() {
requestData("why").then((res1) => {
console.log("第一次结果:", res1);
requestData(res1 + "kobe").then((res2) => {
console.log("第二次结果:", res2);
requestData(res2 + "james").then((res3) => {
console.log("第三次结果:", res3);
});
});
});
}方式二:用 Promise 重构(解决回调地狱)
function getData2() {
requestData("why")
.then((res1) => {
console.log("第一次结果:", res1);
return requestData(res1 + "kobe");
})
.then((res2) => {
console.log("第二次结果:", res2);
return requestData(res2 + "james");
})
.then((res3) => {
console.log("第三次结果:", res3);
});
}方式三:利用生成器
function* getData3() {
const res1 = yield requestData("why");
console.log("res1:", res1);
const res2 = yield requestData(res1 + "kobe");
console.log("res2:", res2);
const res3 = yield requestData(res2 + "james");
console.log("res3:", res3);
}
const generator4 = getData3();
generator4.next().value.then((res1) => {
generator4.next(res1).value.then((res2) => {
generator4.next(res2).value.then((res3) => {
generator4.next(res3);
});
});
});方式四:生成器函数自动化
上面的手动 next 也可以自动执行——写一个递归函数,把每次的结果再传回生成器,直到 done 为止。
function execGenFn(genFn) {
const generator = genFn();
function exec(res) {
const result = generator.next(res);
if (result.done) return;
result.value.then((res) => {
exec(res);
});
}
exec();
}
execGenFn(getData3);这种方式就是 async/await 的雏形。
方式五:async/await
async function getData5() {
const res1 = await requestData("why");
console.log("res1:", res1);
const res2 = await requestData(res1 + "kobe");
console.log("res2:", res2);
const res3 = await requestData(res2 + "james");
console.log("res3:", res3);
}异步函数 async function#
async 是 asynchronous 的缩写,表示异步;sync 是 synchronous 的缩写,表示同步。
async function foo() {}
const bar = async function () {};
const baz = async () => {};
class Person3 {
async running() {}
}异步函数的执行流程:
- 内部代码的执行过程和普通函数一致,默认也是同步执行;
- 有返回值时和普通函数有区别(与 Promise 的三个状态一样):
- 返回值相当于被包裹到
Promise.resolve中; - 如果返回值本身是 Promise,状态由这个 Promise 决定;
- 如果返回值是实现了 thenable 的对象,由对象的 then 方法决定;
- 返回值相当于被包裹到
- 在 async 中抛出异常,程序不会像普通函数那样直接报错,而是作为 Promise 的 reject 传递。
await 关键字:
- 只能在 async 函数内部使用,普通函数里不行;
- 通常后面跟一个返回 Promise 的表达式;
- await 会等 Promise 变成 fulfilled 后,继续执行异步函数;
- 从 await 到下一个 await 之间的代码,可以当作 then 来理解。
返回值规则:
- await 后面是普通值,直接返回这个值;
- await 后面是 thenable 对象,根据对象的 then 方法调用决定后续的值;
- await 后面的 Promise 是 rejected,那么会把 reject 结果作为函数 Promise 的 reject 值。
async function foo2() {
console.log("-------");
const res1 = await bar2();
console.log("await 后面的代码:", res1);
const res2 = await bar2();
console.log("await 后面的代码:", res2);
console.log("+++++++");
}进程与线程#
概念#
- 进程(process):计算机已经运行的程序,是操作系统管理程序的一种方式。可以认为启动一个应用程序就会默认启动一个进程(也可能是多个)。
- 线程(thread):操作系统能够运行运算调度的最小单位,通常被包含在进程中。每一个进程都会启动至少一个线程来执行程序中的代码,这个线程叫主线程。
- 所以也可以说进程是线程的容器。
形象一点:操作系统像一个大工厂,工厂里的车间是进程,车间里的工人是线程。

操作系统的工作方式:CPU 运算速度非常快,可以在多个进程之间快速切换,做到同时运行多个进程的效果。进程中的线程获取到时间片时,就可以快速执行我们编写的代码,用户感受不到这种快速切换。
浏览器中的 JavaScript#
JS 的容器进程是浏览器或者 Node。
- 目前多数浏览器是多进程的:打开一个 tab 页面就会开启一个新的进程,这是为了防止一个页面卡死导致所有页面无法响应、整个浏览器强制退出;
- 每个进程中又有很多线程,其中包括执行 JavaScript 代码的线程;
- JavaScript 是单线程的(可以开启 workers),意味着同一时刻只能做一件事,如果这件事很耗时,当前线程就会被阻塞;
- 所以真正耗时的操作,是交给浏览器的其他线程来完成的。
事件循环#
- JS 解析一段代码时,会把同步代码按顺序排进执行栈,依次执行里面的函数;
- 遇到异步任务时交给其他线程处理;
- 当前执行栈的同步代码全部执行完后,会从队列里取出已完成的异步任务回调,加入执行栈继续执行,遇到异步任务又交给其他线程;
- 如此循环往复。其他异步任务完成后,会把回调放进任务队列,等着执行栈来取。

宏任务、微任务#
事件循环中不是只有一个队列,事实上维护了两个:
- 宏任务队列(macrotask queue):ajax、setTimeout、DOM 监听、UI Rendering 等;
- 微任务队列(microtask queue):Promise 的 then 回调、MutationObserver API、
queueMicrotask()等。
代码的优先级:
- main script 中的代码优先执行(也就是顶层 script 代码);
- 在执行任何一个宏任务之前,都会先查看微任务队列里是否有任务需要执行;
- 也就是说宏任务执行之前必须保证微任务队列是空的,不为空就优先执行微任务队列里的回调。
事件循环相关经典题目#
题目一
console.log("script start");
setTimeout(function () {
console.log("setTimeout1");
new Promise(function (resolve) {
resolve();
}).then(function () {
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then4");
});
console.log("then2");
});
});
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("then1");
});
setTimeout(function () {
console.log("setTimeout2");
});
console.log(2);
queueMicrotask(() => {
console.log("queueMicrotask1");
});
new Promise(function (resolve) {
resolve();
}).then(function () {
console.log("then3");
});
console.log("script end");
// script start → promise1 → 2 → script end
// then1 → queueMicrotask1 → then3
// setTimeout1 → then2 → then4 → setTimeout2题目二
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(function () {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
// script start → async1 start → async2 → promise1 → script end
// async1 end → promise2
// setTimeout写在最后#
这一篇里最「神清气爽」的是生成器那一节:先是自己维护 index 写迭代器,然后用 yield* 一行搞定;再往下,把生成器自动化执行的那段代码改写成 async/await,几乎就是同一件事换了写法。
事件循环的经典题目我当年也抄了两道,答案是对的,但说实话是「背下来的正确」——真正弄懂还是靠后面写项目时踩坑。
下一篇是进阶的最后一篇:额外知识(异常、storage、正则)、手写防抖节流拷贝事件总线,以及网络请求。
