主题
遍历器 iterator与生成器 generator(Day3 4/4)
本页对应 Day03_ES6,共 5 段视频,约 45 分钟。
学习目标
- 能区分遍历器、可遍历对象和伪数组,并分别举例说明三者的关系与差异。
- 能通过
Symbol.iterator获取数组、字符串、Set等对象的遍历器,并调用其next()方法读取遍历结果。 - 能使用
for...of、解构赋值、扩展运算符、Array.from等语法遍历或转换可遍历对象。 - 能编写生成器函数,说明
yield、next()、return在生成器执行过程中的作用。 - 能为普通对象部署
Symbol.iterator接口,使其支持for...of和解构赋值。 - 能分析直接赋值与浅拷贝的引用共享问题,并设计实现深拷贝的可行方案。
知识点
先分清“可遍历对象”和“遍历器”
为什么需要:数组能用 for...of,普通对象却不行;这背后不是“数组特殊”,而是数组部署了遍历器接口。理解这一点,才能解释解构、扩展运算符、Array.from、new Set() 等为什么都偏爱“可遍历对象”。
怎么想:数组本身不是遍历器,但数组“身体里藏着”一个遍历器;准确说,数组部署了 Symbol.iterator 接口。这个接口的值是一个方法,调用后返回遍历器;遍历器再有 next(),每次取出一个值。转写稿里反复纠正的“变利器/可辨利对象”,应统一为“遍历器/可遍历对象”。
怎么做:可以在数组原型上找到 Symbol.iterator,调用它拿到遍历器:
js
const arr = [100, 200, 300, 400, 500];
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 100, done: false }
console.log(it.next()); // { value: 200, done: false }for...of 的本质就是不断调用遍历器的 next(),直到 done 为 true。所以:
js
for (const v of arr) {
console.log(v);
}易错点:Symbol.iterator 不是字符串 "iterator";不能写成 arr.Symbol.iterator,而要用 arr[Symbol.iterator]。另外,普通对象默认没有部署这个接口,所以 for...of 普通对象会报错。
哪些对象可遍历,什么时候会调用接口
为什么需要:ES6 之后,数组、字符串、Set、Map、arguments、NodeList、HTMLCollection 等被统一到“可遍历对象”的管理方式下。这样 for...of、解构、扩展运算符等就能用同一套规则处理它们。
怎么想:判断一个对象能不能被 for...of,不要只看“像不像数组”,而要看它有没有 Symbol.iterator。转写稿中调试字符串、Set 时,表面上看不出方法,但 for...of 能逐个取出字符或集合成员,说明它们都部署了接口。
怎么做:常见会调用遍历器接口的场景包括:
js
const str = "hello";
const set = new Set([100, 200, 300]);
for (const ch of str) console.log(ch);
for (const n of set) console.log(n);
const [a, b] = set; // 解构
const list = [...set]; // 扩展运算符
const arr2 = Array.from(set); // 转数组
const set2 = new Set([1, 2, 3]); // 构造参数要求可遍历数组解构更准确的说法不是“按索引匹配”,而是内部按遍历顺序取值;扩展运算符、Array.from 也是遍历后把成员一个个拿出来。
易错点:伪数组和可遍历对象不是一回事。伪数组是“像数组、有索引和 length,但不是数组”的数据类型;可遍历对象是“部署了遍历器接口”的对象。字符串、arguments、NodeList、HTMLCollection 可能两者兼具;Set、Map 是可遍历对象,但不是伪数组。
生成器:用 function* 生成遍历器
为什么需要:既然可遍历对象的关键是“有一个方法能返回遍历器”,那能不能自己造一个遍历器?生成器就是最方便的办法。
怎么想:生成器是特殊函数,写法是 function*,调用它不会执行函数体,而是返回一个遍历器;调用遍历器的 next() 才执行到 yield,yield 后面的值作为 next() 返回对象的 value。
怎么做:
js
function* gen() {
yield 100;
yield 200;
yield 300;
}
const it = gen();
console.log(it.next()); // { value: 100, done: false }
console.log(it.next()); // { value: 200, done: false }
for (const v of gen()) {
console.log(v); // 100 200 300
}转写稿里的调试过程很关键:在生成器里写 console.log("ok"),只调用 gen() 不会输出;调用 next() 后才执行到第一个 yield 并停下。生成器里的 return 可以提前结束遍历。
易错点:箭头函数不能定义生成器;普通函数里不能写 yield,否则报错。生成器调用后得到的是遍历器,不是普通返回值;不要误以为 return 100 能直接变成函数返回值。
给普通对象部署遍历器接口
为什么需要:普通对象默认不能 for...of。如果希望它也能被遍历,就要手动部署 Symbol.iterator。
怎么想:接口的值必须是“能返回遍历器的方法”,而生成器函数正好满足。转写稿中先给 user[Symbol.iterator] 赋一个空生成器,结果 for...of 不报错但取不到数据;加入 yield 后才真正输出。
怎么做:让对象按属性名和属性值成对输出:
js
const user = { name: "高小乐", age: 18, address: "北京" };
user[Symbol.iterator] = function* () {
for (const key in this) {
yield [key, this[key]];
}
};
for (const item of user) {
console.log(item); // ["name", "高小乐"] ...
}这样 user 就成了可遍历对象,解构、扩展运算符等也能使用:
js
const [a, b] = user;
console.log(a); // ["name", "高小乐"]
console.log([...user]); // 二维数组易错点:yield [key, this[key]] 中 key 是变量,不能写成 yield [key, this.key],否则会去找名为 key 的属性;for...in 遍历的是可枚举属性名。
作业提醒:浅拷贝与深拷贝
转写稿最后用 u01 演示了浅拷贝:const u02 = { ...u01 } 只复制了表面一层。修改 u02.name 不影响 u01,但修改 u02.children[0] 这类嵌套引用,会影响原对象。原因是嵌套对象仍指向同一块内存。作业“实现深拷贝”要求彻底复制嵌套结构;转写稿只提出问题并提示画图理解,没有给出最终实现,这里不编造方案。
先分清:可遍历对象不是遍历器
为什么需要:for...of 能统一遍历数组、字符串、Set 等,但数组本身并不是遍历器。必须先把“可遍历对象”和“遍历器”分开,后面生成器才不会混。
怎么想:数组像“身体里藏着一个遍历器”的对象:它不是遍历器本身,但部署了遍历器接口。调用接口就能取出对应的遍历器,遍历器再有 next()。
怎么做:在控制台展开数组原型,能找到 Symbol.iterator 属性;用中括号取接口并调用:
js
const arr = [100, 200, 300, 400, 500];
const it = arr[Symbol.iterator]();
console.log(it.next()); // { value: 100, done: false }
console.log(it.next()); // { value: 200, done: false }字符串、Set 也类似:
js
const msg = "hello";
const s = new Set([100, 200, 300]);
for (const v of msg) console.log(v);
for (const v of s) console.log(v);易错点:可遍历对象不一定就是遍历器;遍历器一定可遍历。接口属性名不是字符串 "Symbol.iterator",而是 Symbol.iterator 这个 Symbol 值,所以必须写 arr[Symbol.iterator]。转写里反复纠正的“可便利/变利器”,准确说法是“可遍历对象/遍历器”。
哪些操作会调用遍历器接口
为什么需要:知道谁在调用接口,才能解释 for...of、解构、Array.from、扩展运算符为什么都能作用于可遍历对象。
怎么想:这些操作的共同目标是“按顺序把成员一个个取出来”。要取,就先通过 Symbol.iterator 拿到遍历器,再不断调用 next()。
怎么做:常见情况包括:for...of 遍历可遍历对象或遍历器;数组解构;Array.from 转数组;扩展运算符拆分;new Set()、new Map() 等构造参数要求可遍历对象。例如:
js
const arr = [100, 200, 300];
const [a, b] = arr;
console.log([...arr]);
console.log(Array.from(arr));易错点:数组解构不要只说“按索引匹配”,更严谨是“按遍历顺序取出成员”。另外,伪数组和可遍历对象不是一回事:arguments、NodeList、HTMLCollection 等可能既有索引结构又可遍历,但概念来源不同;ES6 之后更强调“是否部署了遍历器接口”。
生成器:专门生产遍历器的特殊函数
为什么需要:手写遍历器要自己维护状态和 next(),很麻烦。生成器把“产出数据”的过程写成 yield,调用后自动得到遍历器。
怎么想:生成器仍是函数,但定义时 function 后加 *;调用它不执行函数体,而是返回遍历器;调用遍历器的 next() 才执行到下一个 yield 并停下。
怎么做:
js
function* gen() {
console.log("ok");
yield 100;
yield 200;
yield 300;
yield { name: "小乐" };
yield 500;
yield 600;
}
const it = gen();
console.log(typeof gen); // function
console.log(it); // 遍历器对象
console.log(it.next()); // 先打印 ok,再得到 { value: 100, done: false }也可以直接:
js
for (const v of gen()) {
console.log(v);
}调试时把 console.log("ok") 放在第一个 yield 前,会发现:只调用 gen() 不打印;调用 it.next() 才打印,并且执行到第一个 yield 就停。再 next() 才继续到下一个 yield。生成器里的 return 会结束遍历。
易错点:箭头函数不能定义生成器;yield 只能写在生成器函数里,普通函数写 yield 会报错;调用生成器得到的是遍历器,不是普通 return 值;yield 后面的数据会成为 next() 返回对象中 value 的值。
给普通对象部署遍历器接口
为什么需要:普通对象默认不能 for...of。给对象部署 Symbol.iterator,就能把它变成可遍历对象,这是生成器很实用的目的。
怎么想:接口的值必须是一个“能返回遍历器”的函数,生成器函数正好满足。对象里用 for...in 拿属性名,再 yield [key, this[key]] 产出键值对。
怎么做:
js
const user = { name: "高小乐", age: 18, address: "北京" };
// 未部署前:for (const v of user) {} 会报 user is not iterable
user[Symbol.iterator] = function* () {
for (const key in this) {
yield [key, this[key]];
}
};
for (const v of user) {
console.log(v); // ["name", "高小乐"] ...
}部署后还能解构、扩展:
js
const [first] = user;
console.log(first);
console.log([...user]); // 二维数组易错点:属性名要写 [Symbol.iterator];取属性值要写 this[key],不能写 this.key,否则会把 key 当成固定属性名。转写里的调试过程是:先 for...of 报错;部署空生成器后不报错但取不到数据;加上 yield 后才输出。
作业:浅拷贝与深拷贝
为什么需要:复制对象时,如果直接 const u02 = u01,只是两个变量指向同一个对象,改一个另一个也变。
怎么想:{ ...u01 } 会创建新对象,但只复制第一层,嵌套的数组/对象仍共享引用,这叫浅拷贝。
怎么做:
js
const u01 = {
name: "高小乐",
children: ["司马姥姥", "欧阳姥姥"],
parents: { father: "高启强" }
};
const u02 = { ...u01 };
u02.name = "大杯高小乐";
console.log(u01.name); // 不受影响
u02.children[0] = "西门姥姥";
console.log(u01.children[0]); // 被影响,仍是浅拷贝易错点:扩展运算符不是深拷贝;只改第一层属性看不出问题,一改嵌套数组/对象就会暴露共享引用。作业要求先想清楚“为什么只拷了一层”,再尝试实现彻底深拷贝;转写没有给出最终实现,不要把它当成已经完成的答案。
课堂画面





代码示例
示例一:可遍历对象与遍历器接口(Symbol.iterator)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>可遍历对象</title>
</head>
<body>
<script>
const arr = [100, 200, 300, 400, 500];
const msg = 'hello world';
const s = new Set(arr);
/* 1. 可遍历对象身上“藏”着 Symbol.iterator 接口,它的值是一个方法 */
console.log(arr[Symbol.iterator]); // function values() { ... }
/* 2. 调用接口 → 得到一个遍历器,遍历器身上有 next() */
const it = arr[Symbol.iterator]();
console.log(it.next()); // {value: 100, done: false}
console.log(it.next()); // {value: 200, done: false}
/* 3. for...of 既可以遍历遍历器,也可以遍历可遍历对象 */
for (const v of arr) console.log('arr', v);
for (const v of msg) console.log('msg', v);
for (const v of s) console.log('set', v);
/* 4. 其它会调用遍历器接口的场合 */
const [a1, a2] = s; // 解构赋值:按遍历顺序,不是按索引
console.log(a1, a2);
console.log([...msg]); // 扩展运算符
console.log(Array.from(s)); // 转成纯数组
console.log(new Set(msg)); // Set 的参数要求是“可遍历对象”
</script>
</body>
</html>要点
Symbol.iterator是属性名(Symbol 类型的值),不能写成arr.iterator,必须用中括号:arr[Symbol.iterator]。- 接口的值是一个方法,方法的返回值才是遍历器;所以要写成
arr[Symbol.iterator](),少了括号拿到的是函数本身。 - 遍历器一定有
next(),返回{value, done};for...of内部就是不断调用next(),这一步不需要手动写。 - 解构赋值之所以能用于 Set、字符串,是因为它们都是可遍历对象;说“按索引匹配”并不严谨,准确说法是按遍历顺序依次取值。
示例二:生成器 function* 与 yield
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>生成器</title>
</head>
<body>
<script>
/* function 后面加星号才是生成器,箭头函数不能定义生成器 */
function* gn() {
console.log('ok');
yield 100;
yield 200;
yield 300;
yield { name: '小乐' };
yield 500;
yield 600;
}
console.log(typeof gn); // function —— 生成器本质还是函数
/* 调用生成器:函数体一句都不执行,只得到一个遍历器 */
const it = gn();
console.log(it.next()); // 先输出 ok,再 {value: 100, done: false}
console.log(it.next()); // {value: 200, done: false}
/* for...of 内部不停调用 next(),把 yield 的值依次取出 */
for (const v of gn()) console.log(v);
/* return 可以提前结束生成器 */
function* gn2() {
yield 1;
yield 2;
return 100;
yield 3;
}
console.log([...gn2()]); // [1, 2]
/* 非生成器函数里写 yield 直接语法报错 */
// function fn() { yield 1; } // SyntaxError
</script>
</body>
</html>要点
- 生成器必须写成
function* 名字(),箭头函数不能定义生成器;yield只能出现在带星号的生成器函数里,普通函数里写yield直接报错。 - 调用生成器不会执行函数体,只返回一个遍历器;只有调用
next()才执行,并且执行到一个yield就停下,下次next()从停下的位置继续。 - 生成器的返回值无法自定义(写
return也一样),return的作用是让遍历提前结束,done变为true。 yield后面的数据会成为next()返回对象里value的值。
示例三:用生成器给普通对象部署遍历器接口
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>给对象部署遍历器接口</title>
</head>
<body>
<script>
const user = {
name: '高小乐',
age: 18,
address: '北京',
a: 1,
b: 2
};
/* 部署前:普通对象不是可遍历对象 */
// for (const v of user) {} // TypeError: user is not iterable
// console.log([...user]); // 报错
// console.log(Array.from(user)); // 报错
// new Set(user); // 报错
/* 部署:给对象加 Symbol.iterator,值是一个生成器 */
user[Symbol.iterator] = function* () {
for (const prop in this) { // this 指向调用该方法的对象 user
yield [prop, this[prop]]; // 产出 [属性名, 属性值] 键值对
}
};
/* 部署后:所有需要可遍历对象的场合都能用 */
for (const v of user) console.log(v); // ['name','高小乐'] ...
console.log([...user]); // 二维数组
const [e1, e2] = user; // 解构赋值
console.log(e1, e2);
</script>
</body>
</html>要点
- 关键一行是
user[Symbol.iterator] = function* () {...}:属性名必须用中括号写法,值是生成器(能产生遍历器的函数),不能写成普通函数。 - 取属性值必须用
this[prop],不能写成this.prop——点语法会把prop当成属性名,结果全是undefined。 for...in里this指向调用该方法的对象(这里是user);Symbol类型的属性不会被for...in遍历到,所以不会死循环。- 部署完之后,
for...of、解构赋值、扩展运算符、Array.from、new Set都能作用于该对象——它们本质上都是调用了Symbol.iterator拿到遍历器。
常见错误
对普通对象直接
for...of,报is not iterable
现象→运行即报错;原因→对象未部署Symbol.iterator;修法→在对象或其原型上添加[Symbol.iterator]生成器方法,再遍历。调用生成器后以为函数体立即执行,或把
return当迭代数据
现象→console.log不输出、next()才逐步执行,return后迭代提前结束;原因→生成器调用只返回迭代器,函数体由next()驱动,return结束迭代;修法→用next()/for...of驱动,产出数据统一用yield。在普通函数或箭头函数里写
yield
现象→直接语法报错;原因→yield只能出现在生成器函数体;修法→改为function*,箭头函数不能定义生成器。部署迭代器接口时写成
user.Symbol.iterator或接口值直接给数组/对象
现象→for...of仍不可用或运行时报错;原因→属性名必须是[Symbol.iterator],且其值应是返回迭代器的方法;修法→写成[Symbol.iterator]: function* () { ... },确保调用后得到迭代器。把扩展运算符复制对象当成深拷贝
现象→修改副本的嵌套对象,原对象也被改;原因→扩展运算符只复制第一层,嵌套引用仍共享;修法→递归拷贝、structuredClone或按作业要求实现深拷贝并验证嵌套层。for...of与for...in混用
现象→对象遍历拿到属性名、数组遍历拿到索引字符串;原因→for...in遍历可枚举属性,for...of依赖迭代器;修法→数组/可迭代对象用for...of,对象键值对用Object.entries等明确方式。
练习
改错题:下面代码在普通函数中使用了
yield,请改为生成器,并写出连续调用next()时每次的输出与done状态。jsfunction gen() { yield 1; yield 2; }给一个普通对象部署
Symbol.iterator,要求for...of每次输出[属性名, 属性值],且不能把属性名写死。再用数组解构和扩展运算符各验证一次。用生成器实现
range(start, end, step),要求支持for...of遍历,并能被解构和扩展运算符消费。判断并验证:数组、字符串、
Set、Map、arguments、普通对象、仅含length和索引的类数组对象,哪些可直接for...of?普通对象若不能,如何改造?深拷贝练习:给定含数组和嵌套对象的
user01,分别用扩展运算符和深拷贝实现副本;写测试证明修改副本的嵌套数据不会污染原对象,并说明扩展运算符为什么不够。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 可遍历对象 | 16:42 |
| 2 | 遍历器、可遍历对象和伪数组 | 04:00 |
| 3 | 生成器 | 09:54 |
| 4 | 生成器 | 09:59 |
| 5 | 作业-实现深拷贝 | 04:27 |
小结
- 可迭代对象的关键是部署
Symbol.iterator,调用后返回迭代器;for...of、解构、扩展、Array.from、Set构造等都会消费它。 - 迭代器通过
next()返回{ value, done };生成器调用后返回的正是迭代器。 function*是定义生成器的常用方式,yield只能出现在生成器函数体,箭头函数不能定义生成器。- 生成器的价值之一是给普通对象部署迭代器接口,使其获得
for...of、解构、扩展等能力。 - 可迭代对象与伪数组不是同一概念:伪数组不一定可迭代,可迭代对象也不一定是伪数组。
- 生成器中的
return会提前结束迭代,不应把它当作yield来产出数据。 - 浅拷贝只复制第一层,嵌套引用仍共享;深拷贝必须处理嵌套结构并用测试验证。