Skip to content

Class 语法与Set 和 Map(Day3 3/4) ​

本页对应 Day03_ES6,共 6 段视频,约 1 小时 21 分。

学习目标 ​

  1. 能继承内置 Array 类,使用 super(...args) 正确传递构造参数,并为子类实例添加自有属性,验证实例仍具备数组行为。
  2. 能创建 Set,使用 add/delete/has/clear/forEach/size,说明成员唯一性,并用扩展运算符或 Array.from 完成数组去重且返回新数组。
  3. 能区分 WeakSet 与 Set,创建 WeakSet 并验证其成员只能为对象、不可遍历、仅提供 add/delete/has。
  4. 能创建 Map,使用二维数组初始化,调用 set/get/has/delete/clear/forEach/size,说明键可为任意类型,并验证对象或数组作为键时的引用相等性。
  5. 能区分 WeakMap 与 Map,验证其键只能为对象、不可遍历,并使用 set/get/has/delete 完成基本操作。
  6. 能获取数组、Set、Map 的遍历器对象,调用 next() 读取 value/done,并用 for...of 或循环消费遍历器,说明指针耗尽后不可复用。

知识点 ​

继承内置类:从 Array 扩展出自己的数组 ​

为什么需要:内置构造函数(Array、Date 等)能力固定,但业务里常想要“数组 + 额外属性/方法”。转写稿里“得税个税”是同音误写,实际是 extends Array。

怎么想:子类继承 Array 后,数组特性天然保留;但一旦重写 constructor,就必须先 super(...),否则不能使用 this,且父类构造参数没传进去,实例会变空。

怎么做:

js
class MyArray extends Array {
  constructor(name, ...args) {
    super(...args);      // 关键:把剩余参数拆开传给 Array
    this.name = name;    // 再加自己的属性
  }
}

const ma = new MyArray('小圆', 1, 2, 3, 4, 5);
console.log(ma);         // [1,2,3,4,5],name: '小圆'
console.log(ma.length);  // 5

易错点:写成 super(args) 会把整个数组当成一个元素;写成 super() 再赋值则数组为空。转写稿的调试过程就是先报错、再加 super、再发现“整体传进去”不对,最后改成 super(...args)。

Set:值唯一的集合 ​

为什么需要:数组允许重复,做“已执行任务”“已添加信息”时每次都要手动判断是否存在。Set 的成员唯一,重复添加自动失效。

怎么想:Set 类似数组,但存纯值、不存键值对;构造函数接收可迭代对象(数组、字符串等),自动去重;实例有 size,没有索引。

怎么做:

js
const s1 = new Set([100, 200, 200, 300, 400]);
console.log(s1.size); // 4
s1.add(250);
s1.delete(200);
console.log(s1.has(250)); // true

const arr = ['司马老姥', '欧阳老姥', '司马老姥', '东方老姥'];
const newArr = [...new Set(arr)]; // 数组去重

易错点:Set 只能 new,直接 Set() 调用会报错;两个直接写出的对象字面量不是同一个对象,不会去重;删除只能按值删,不能按索引删。

WeakSet:只能装对象的弱集合 ​

为什么需要:有些场景只想存对象引用,不希望因为集合强引用导致对象无法回收;同时要求成员不能是原始值。

怎么想:它和 Set 一样“值不重复”,但成员只能是对象类型;不能遍历,也没有 size,操作只有 add/delete/has。

怎么做:

js
const ws = new WeakSet();
const msg = new String('hello');
ws.add(msg);
ws.add(document.body);
console.log(ws.has(document.body)); // true
ws.delete(msg);

易错点:ws.add(100)、ws.add('hello') 会报错;直接写对象字面量后再 has 另一个“长得一样”的对象,结果通常是 false,因为不是同一引用。

Map:键不限类型的键值集合 ​

为什么需要:Object 的属性名只能是字符串或 Symbol;若想用数组、对象、Set 当“键”记录信息,就不够灵活。Map 的键可以是任意类型。

怎么想:Map 类似 Object 的键值对集合,但构造参数必须是二维数组:每个元素是 [key, value];set 既可新增也可修改,get/has/delete/clear 按 key 操作。

怎么做:

js
const arr = [1, 2];
const user = { name: '小乐' };
const s = new Set([1, 2]);

const m = new Map([
  [arr, [250, 350]],
  [user, '上海'],
  [s, ['司马老姥', '欧阳老姥']],
  ['hello', 'world'],
  [100, '纯数字键']
]);

console.log(m.size);       // 5
m.set(arr, 250);           // 修改
console.log(m.get(arr));   // 250
console.log(m.has(user));  // true
m.delete(100);

易错点:new Map(100) 这种参数不合法;m.get([1,2]) 取不到,因为新数组不是原来的键;forEach 回调第一个参数是 value,第二个才是 key。

WeakMap:键只能是对象的弱映射 ​

为什么需要:和 WeakSet 类似,希望用对象做键记录附加信息,又不强引用对象;键不能是原始值,且不需要遍历。

怎么想:WeakMap 与 Map 相似,但键必须是对象类型;不可遍历,没有 size,方法只有 set/get/has/delete。

怎么做:

js
const wm = new WeakMap();
const key = document.body;
wm.set(key, 'body 的信息');
console.log(wm.get(key)); // body 的信息
console.log(wm.has(key)); // true
wm.delete(key);

易错点:wm.set('hello', 1) 会报错;不能 for...of,也没有 forEach;用新对象字面量当键去 get 通常取不到。

遍历器对象:next 与 for...of ​

为什么需要:数组、字符串、Set、Map 都能遍历,但内部结构不同。ES6 用统一“遍历器接口”提供统一访问机制。

怎么想:arr.values()、set.values()、map.entries() 返回的是遍历器对象,不是数组。它内部有指针,每次 next() 返回 { value, done };done 为 true 表示到底。

怎么做:

js
const arr = [100, 200, 300];
const it = arr.values();
console.log(it.next()); // { value: 100, done: false }
console.log(it.next()); // { value: 200, done: false }

for (const v of arr.entries()) {
  console.log(v); // [0,100] [1,200] [2,300]
}

易错点:遍历器指针走到底后不能自动复位,再 next() 仍是 done: true;for...of 会消费遍历器,遍历完再 next() 没有意义;遍历器对象不是数组,不能直接当数组用。

继承内置类:从“数组扩展”出发 ​

为什么需要:内置构造函数(Array、Date 等)能力有限,若想保留数组全部能力,又增加自己的属性或方法,继承内置类比“从零造一个数组”更稳。怎么想:子类先继承 Array,数组特性自然具备;要加名字等内部属性,就在构造器里先接收名字,再把剩余参数交给父类。怎么做:

js
class MyArray extends Array {
  #name;
  constructor(name, ...args) {
    super(...args); // 等价于把 100,200,300 拆开传给 Array
    this.#name = name;
  }
  getName() {
    return this.#name;
  }
}

const ma = new MyArray("小圆", 100, 200, 300);
console.log(ma); // 有数组结构、length、原型方法
console.log(ma.getName()); // 小圆

调试时最容易写成 super(args),结果整个数组变成一个元素;必须用扩展运算符 super(...args)。易错点:super 必须在 this 之前调用;私有字段要先声明;继承后实例仍是 Array 体系,但自定义属性不要和数组索引混淆。

Set:值唯一的集合 ​

为什么需要:实际开发常要“存过就不再存”,例如任务队列、缓存标记、数组去重。怎么想:Set 类似数组,但成员值唯一,且没有索引,不能靠下标取值。怎么做:

js
const s = new Set([100, 200, 200, 300, 400]);
console.log(s.size); // 4
s.add(250);
s.delete(200);
console.log(s.has(250)); // true
s.forEach((item) => console.log(item));

数组去重可借助可迭代对象展开:

js
const arr = ["司马姥姥", "欧阳姥姥", "司马姥姥", "东方姥姥"];
const unique = [...new Set(arr)];
// 也可:Array.from(new Set(arr))

易错点:new Set(100) 会报错,参数应是可迭代对象;对象成员即使长得一样也算不同引用;Set 无索引,不能写 s[0];add 已存在的值不会重复添加。

WeakSet:只能装对象的弱集合 ​

为什么需要:有些场景只想记录对象是否出现过,不希望因为集合引用阻止对象回收。怎么想:它像 Set,但成员只能是对象类型,且不可遍历。怎么做:

js
const ws = new WeakSet();
const msg = new String("hello");
ws.add(msg);
ws.add(document.body);
console.log(ws.has(msg)); // true
ws.delete(msg);

易错点:ws.add(100) 会报错;不能 for...of,没有 size;直接写对象字面量再判断,通常不是同一个引用,删不掉也判不出。

Map:键不限字符串的键值集合 ​

为什么需要:Object 的属性名主要是字符串或 Symbol;若要拿数组、对象、Set 当“键”,Map 更合适。怎么想:Map 是键值对集合,键和值都可是任意类型;构造参数是二维数组。怎么做:

js
const arrKey = [1, 2];
const user = { name: "小乐" };
const m = new Map([
  [arrKey, 250],
  [user, "上海"],
  ["hello", "world"],
  [1, "数字键"]
]);

m.set(arrKey, 300); // 修改
m.set({ name: "新对象" }, 350); // 新键,新增
console.log(m.get(arrKey)); // 300
console.log(m.has(user)); // true
m.forEach((value, key) => console.log(key, value));

易错点:new Map(100) 不行;构造参数必须是键值对序列;用新对象当键去 get/has/delete 通常失败,因为引用不同;set 对已存在键是修改,不存在才是新增。

WeakMap:键必须是对象的弱映射 ​

为什么需要:当键是对象、又不想强引用它时,WeakMap 比 Map 更合适。怎么想:它像 Map,但键只能是对象,且不可遍历。怎么做:

js
const wm = new WeakMap();
const key = {};
wm.set(key, "缓存信息");
console.log(wm.get(key));
console.log(wm.has(key));
wm.delete(key);

易错点:wm.set("hello", 1) 会报错,因为键不是对象;值可以是任意类型;没有 size,不能 forEach,也不能 for...of。

遍历器对象:next、done 与 for...of ​

为什么需要:数组、Set、Map 都想被统一遍历,遍历器提供统一访问机制。怎么想:调用 values()、keys()、entries() 得到遍历器对象;它内部有指针,每次 next() 取出当前值并后移。怎么做:

js
const arr = [100, 200, 300];
const it = arr.values();
console.log(it.next()); // { value: 100, done: false }
console.log(it.next()); // { value: 200, done: false }
console.log(it.next()); // { value: 300, done: false }
console.log(it.next()); // { value: undefined, done: true }

for (const v of arr.entries()) {
  console.log(v); // [0,100] [1,200] [2,300]
}

易错点:遍历器对象不是数组;for...of 会消耗指针,遍历完再 next() 没有有效值;entries() 每次给的是 [key, value];WeakSet、WeakMap 不可遍历。

为什么需要遍历器 ​

数组、字符串、Set、Map 等数据都能被遍历,但它们的类型不同、内部结构也不同。如果每种数据都各写一套取值规则,使用成本会很高。遍历器(iterator)要解决的就是:为不同数据类型提供统一的访问机制。只要某个数据部署了 iterator 接口,就可以用统一方式逐个取出成员。注意:数组本身不是遍历器,但数组部署了遍历器接口;调用数组的 values()、keys()、entries() 会得到一个遍历器对象,由这个对象负责逐个取值。

怎么想:先拿到遍历器对象 ​

不要一上来就背 next(),先分清两个角色:可遍历对象和遍历器对象。数组、Set、Map 是可遍历对象;对它们调用 values() 等方法后,返回的才是遍历器对象。转写稿中先创建数组,再调用 values(),打印结果看到类似 Array Iterator,说明它不是数组,而是遍历器对象。调试时先打印遍历器对象,再调用 next() 看返回值,这样能把“数据本身”和“取值工具”分开。

js
const arr = [100, 200, 300, 400, 500, 600, 700];

const values = arr.values();
console.log(values); // Array Iterator

怎么做:用 next() 逐个取值 ​

遍历器对象内部有一个指针,初始指向第一个数据。每次调用 next(),取出当前指针指向的数据,然后指针下移。next() 返回一个对象,里面有 value 和 done:value 是当前值,done 表示是否已经遍历结束。第一次调用得到 { value: 100, done: false };继续调用会依次得到 200、300……直到最后一个值取完后,再调用才得到 done: true。

js
const values = arr.values();

console.log(values.next()); // { value: 100, done: false }
console.log(values.next()); // { value: 200, done: false }

怎么做:用循环或 for...of 消费遍历器 ​

手写很多次 next() 很傻,可以用循环反复调用,并在 done 为 true 时 break。转写稿中用 while (true) 配合 break 演示:每轮先 next(),判断 data.done,结束就跳出,否则输出 data.value。更简洁的是 for...of,它会自动调用遍历器对象的 next(),把每次的 value 交给循环变量。

js
const keys = arr.keys();
while (true) {
  const data = keys.next();
  if (data.done) break;
  console.log(data.value); // 0 1 2 3 4 5 6
}
js
for (const item of arr.values()) {
  console.log(item); // 100 200 ... 700
}

for (const item of arr.entries()) {
  console.log(item); // [0,100] [1,200] ...
}

易错点:next 返回的不是值本身 ​

next() 返回的是对象,不是直接返回 100。所以不能写 const v = arr.values().next() 就以为 v 是 100;要取 v.value。另外,done 为 true 表示遍历结束,此时不要再把 value 当有效数据。遍历器对象是一次性的:for...of 走完后,指针已经到底,再调用 next() 没有意义;同一个遍历器对象再 for...of 也不会重新从头开始。想重新遍历,要重新调用 values() 等方法生成新的遍历器对象。

易错点:keys、values、entries 结果不同 ​

数组的 keys() 得到索引,values() 得到元素值,entries() 得到 [索引, 值]。Set 没有索引结构,所以它的 keys() 和 values() 结果一样;Map 的 entries() 得到 [key, value]。转写稿还强调:WeakSet、WeakMap 不可遍历;Object.keys()、Object.values()、Object.entries() 得到的是纯数组,不是遍历器对象,只是结构看起来接近。把这些区别记住,比单独背某个方法更重要。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

课堂画面 6

代码示例 ​

示例 1:继承内置类 Array ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>继承内置类</title>
</head>
<body>
<script>
  class MyArray extends Array {
    #name; // 私有字段,必须在类体内声明

    constructor(name, ...args) {
      super(...args);   // 展开后调用 Array 的构造函数
      this.#name = name;
    }

    get name() {
      return this.#name;
    }
  }

  const ma = new MyArray('小圆', 100, 200, 300, 400, 500, 600);

  console.log(ma);                 // MyArray(6) [100, 200, 300, 400, 500, 600]
  console.log(ma.length);          // 6
  console.log(ma.name);            // 小圆
  console.log(ma instanceof Array);// true
  console.log(ma.map(x => x * 2)); // MyArray(6) [200, 400, 600, 800, 1000, 1200]
  console.log(ma.push(700), ma.length); // 7 7
</script>
</body>
</html>

关键行与易错点

  • class MyArray extends Array:继承内置构造函数后,数组的特性(length、push、map 等)全部继承下来,再往上加自己的东西。
  • constructor(name, ...args):...args 把 100,200,... 收集成一个数组。
  • super(...args):相当于调用 Array 的构造函数;必须展开,若写成 super(args),整个数组会成为一个元素,length 变成 1。
  • 子类构造器里 super() 之前不能使用 this,不写 super 直接报错。
  • #name 私有字段要在类体内先声明,外部访问 ma.#name 会报语法错误,只能通过 get 暴露。

示例 2:Set / Map 与 WeakSet / WeakMap ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Set 与 Map</title>
</head>
<body>
<script>
  /* ---------------- Set ---------------- */
  const s1 = new Set([100, 200, 200, 300, 400, { name: '小圆' }, { name: '小圆' }]);
  console.log(s1.size);        // 5:两个字面量对象不算重复

  s1.add(250);                 // 添加
  s1.add(100);                 // 已存在,加不进去
  s1.delete(200);              // 删除,只能按值删
  console.log(s1.has(250));    // true
  s1.forEach((item, index) => console.log(item, index)); // 第二个参数不是索引,Set 没有索引结构

  // 数组去重
  const arr = ['司马姥姥', '欧阳姥姥', '司马姥姥', '欧阳姥姥', '东方姥姥', '欧阳姥姥'];
  const newArr = [...new Set(arr)];              // 扩展运算符拆开再包回数组
  console.log(newArr);                           // ['司马姥姥', '欧阳姥姥', '东方姥姥']
  console.log(Array.from(new Set(arr)));         // 同上

  /* ---------------- Map ---------------- */
  const arrKey  = [1, 2, 3];
  const userKey = { name: '小乐' };
  const setKey  = new Set(arrKey);

  const m = new Map([
    [arrKey,  arrKey],
    [userKey, '上海'],
    [setKey,  ['司马姥姥', '欧阳姥姥']],
    ['字符串', 'hello world'],
    [123,     456]
  ]);
  console.log(m.size);            // 5
  console.log(m.get(arrKey));     // [1, 2, 3]

  m.set(arrKey, 250);             // key 已存在 → 修改
  console.log(m.get(arrKey));     // 250

  m.set({ name: '新对象' }, 350); // key 不存在 → 添加
  console.log(m.size);            // 6
  console.log(m.has(arrKey));     // true
  console.log(m.has([1, 2, 3]));  // false:新数组,引用不同

  m.delete(123);
  m.forEach((value, key) => console.log(key, value)); // 第一个是 value,第二个才是 key

  /* ---------------- WeakSet / WeakMap ---------------- */
  const ws = new WeakSet([[], {}, document.body]);
  ws.add(document.body);            // 已存在,加不进去
  console.log(ws.has(document.body)); // true
  console.log(ws.has({}));            // false:新对象

  const wm = new WeakMap([
    [arrKey,  '值可以是任意类型'],
    [userKey, 123]
  ]);
  console.log(wm.get(userKey));     // 123
  // wm.size、wm.forEach、for...of 都不存在,WeakMap 不可遍历
</script>
</body>
</html>

关键行与易错点

  • new Set(...) / new Map(...):只能实例化不能调用;Set 参数是一个数组或可迭代对象,写成 new Set(100, 200) 会报错。
  • Set 成员不能重复,但两个长得一样的对象不算重复——每写一次字面量就是新对象;只有原始类型才“长得一样就相等”。
  • Set 没有索引,不能用 s1[0] 取值,forEach 的第二个参数不是索引;只能用 size 看长度。
  • [...new Set(arr)] 或 Array.from(new Set(arr)) 才能既去重又拿回纯数组。
  • Map 的参数必须是二维数组 [[k, v], ...],key 可以是任意类型;get / has / delete 都是按引用比对,写个新数组 [1,2,3] 取不到值。
  • set 对已有 key 是修改、对没有的 key 是添加;WeakSet 成员只能是对象类型,WeakMap 只有 key 要求对象,value 任意,两者都不可遍历、没有 size。

示例 3:遍历器对象 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, 600, 700];

  /* ---- 由数组的值组成的遍历器对象 ---- */
  const valueIt = arr.values();
  console.log(valueIt);          // Array Iterator {}
  console.log(valueIt.next());   // {value: 100, done: false}
  console.log(valueIt.next());   // {value: 200, done: false}

  /* ---- while + next 手动遍历(keys 得到索引)---- */
  const keyIt = arr.keys();
  while (true) {
    const data = keyIt.next();
    if (data.done) break;        // 指针指空了,结束
    console.log(data.value);     // 0 1 2 3 4 5 6
  }

  /* ---- entries 得到 [索引, 值] ---- */
  const entriesIt = arr.entries();
  for (const v of entriesIt) {
    console.log(v);              // [0,100] [1,200] ... [6,700]
  }

  /* ---- 遍历器只有一根指针,用完就废 ---- */
  const it = new Set([1, 2, 3]).values();
  for (const v of it) console.log(v);  // 1 2 3
  console.log(it.next());              // {value: undefined, done: true}
  for (const v of it) console.log(v);  // 什么也不输出

  /* ---- Map 的 entries 遍历器 ---- */
  const mapIt = new Map([['a', 1], ['b', 2]]).entries();
  for (const [k, v] of mapIt) {
    console.log(k, v);           // a 1  b 2
  }
</script>
</body>
</html>

关键行与易错点

  • arr.values() 返回的是遍历器对象(Array Iterator),不是数组,别当成数组用。
  • 遍历器内部有一根指针,初始指向第一个数据;next() 返回 {value, done},取完当前值后指针才下移。
  • 取到最后一个值 700 时 done 仍是 false,再调一次 next() 才是 {value: undefined, done: true}。
  • 手动遍历必须用 while (true) + if (data.done) break,否则死循环。
  • for...of 内部就是在不停调用 next();指针到底后再 for...of 或 next() 都拿不到数据——这跟数组 forEach 可以反复遍历不一样。
  • keys() 得到索引、values() 得到值、entries() 得到 [索引, 值];Set 的 keys 和 values 结果相同,Map 的三个方法才是三种不同内容。

常见错误 ​

  1. 现象:子类继承 Array 后重写 constructor,访问 this 或设置属性时报错。
    原因:子类 constructor 中必须先调用 super,否则不能使用 this。
    修法:先 super(...args),再设置自有属性;参数用 ...args 收集,传给父类时再展开。

  2. 现象:new Set(100, 200) 报错或结果不符合预期。
    原因:Set 构造参数只能是一个可迭代对象,不是多个参数列表。
    修法:传数组、字符串、Set 等单一可迭代对象;多个值先组成数组再传入。

  3. 现象:set.delete({ name: 'x' }) 删不掉,map.get({ id: 1 }) 取不到。
    原因:对象按引用比较,每次写对象字面量都会创建新引用。
    修法:把对象存入变量,后续 add/set/delete/get/has 使用同一引用。

  4. 现象:new WeakSet([1, 2]) 或 new WeakMap([[1, 'a']]) 报错。
    原因:WeakSet 成员、WeakMap 键必须是对象类型。
    修法:改用对象、数组、函数等引用;原始类型改用 Set/Map。

  5. 现象:对 WeakSet/WeakMap 调 size、forEach、for...of 或展开。
    原因:弱引用结构不可枚举,也没有 size。
    修法:只使用其有限 API;需要遍历或计数时改用 Set/Map。

  6. 现象:同一个 iterator 第二次 for...of 没有输出,或 next() 一直 done: true。
    原因:遍历器内部指针是一次性的,已被消费到底。
    修法:重新调用 values()/keys()/entries() 获取新遍历器,不要复用已耗尽的实例。

  7. 现象:for (const v of set.values()) 中把 v 当成 { value, done }。
    原因:for...of 取的是每次 next().value,不是整个结果对象。
    修法:手动 next() 时才判断 done/value;for...of 中直接处理值。

练习 ​

  1. 定义 MyArray extends Array,构造时接收一个 name 和任意多个数字,实例既要保留数组特性,又要拥有 name 自有属性。
  2. 用 Set 完成数组去重并返回新数组,分别用扩展运算符和 Array.from 实现;再验证对象元素为何不会按内容去重。
  3. 创建一个 Map,分别用数组、对象、Set、字符串、数字作键,演示 set/get/has/delete;对比“同一对象引用”和“新建对象字面量”的 has 结果。
  4. 写函数 consume(iterable):手动调用 iterator.next() 收集值,遇到 done 停止;再用 for...of 写一版,并观察同一个 iterator 被消费两次的结果。
  5. 设计一个 WeakMap 缓存:以 DOM 元素或普通对象为键存元数据,验证原始类型作键会报错,并说明为什么不能遍历它。

原视频对照 ​

#视频时长
1继承内置类07:27
2Set23:41
3WeakSet08:52
4Map16:27
5WeakMap04:14
6遍历器对象20:53

小结 ​

  1. 继承内置类的关键是构造链:先 super 再 this,参数收集与展开必须成对处理。
  2. Set 是唯一值集合,不是索引数组;对象字面量不会因为“长得一样”而相等。
  3. Map 的键可为任意类型,但对象键同样按引用比较,查询和修改必须使用同一引用。
  4. WeakSet/WeakMap 的“弱”伴随限制:键或成员限对象、不可遍历、无 size。
  5. iterator 是一次性游标;next() 返回 { value, done },for...of 本质是在消费这个游标。
  6. 需要重复遍历时,应重新获取 iterator;转数组可用扩展运算符或 Array.from,前提是可迭代。
  7. 选型结论:去重和成员判断优先 Set,复杂键映射优先 Map,对象生命周期关联且无需枚举时用 Weak 系列。
  8. 排错先问三件事:按值还是比较引用、是否可迭代、iterator 是否已被消费。