主题
Class 语法与Set 和 Map(Day3 3/4)
本页对应 Day03_ES6,共 6 段视频,约 1 小时 21 分。
学习目标
- 能继承内置
Array类,使用super(...args)正确传递构造参数,并为子类实例添加自有属性,验证实例仍具备数组行为。 - 能创建
Set,使用add/delete/has/clear/forEach/size,说明成员唯一性,并用扩展运算符或Array.from完成数组去重且返回新数组。 - 能区分
WeakSet与Set,创建WeakSet并验证其成员只能为对象、不可遍历、仅提供add/delete/has。 - 能创建
Map,使用二维数组初始化,调用set/get/has/delete/clear/forEach/size,说明键可为任意类型,并验证对象或数组作为键时的引用相等性。 - 能区分
WeakMap与Map,验证其键只能为对象、不可遍历,并使用set/get/has/delete完成基本操作。 - 能获取数组、
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:继承内置类 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 的三个方法才是三种不同内容。
常见错误
现象:子类继承
Array后重写constructor,访问this或设置属性时报错。
原因:子类constructor中必须先调用super,否则不能使用this。
修法:先super(...args),再设置自有属性;参数用...args收集,传给父类时再展开。现象:
new Set(100, 200)报错或结果不符合预期。
原因:Set构造参数只能是一个可迭代对象,不是多个参数列表。
修法:传数组、字符串、Set 等单一可迭代对象;多个值先组成数组再传入。现象:
set.delete({ name: 'x' })删不掉,map.get({ id: 1 })取不到。
原因:对象按引用比较,每次写对象字面量都会创建新引用。
修法:把对象存入变量,后续add/set/delete/get/has使用同一引用。现象:
new WeakSet([1, 2])或new WeakMap([[1, 'a']])报错。
原因:WeakSet成员、WeakMap键必须是对象类型。
修法:改用对象、数组、函数等引用;原始类型改用Set/Map。现象:对
WeakSet/WeakMap调size、forEach、for...of或展开。
原因:弱引用结构不可枚举,也没有size。
修法:只使用其有限 API;需要遍历或计数时改用Set/Map。现象:同一个 iterator 第二次
for...of没有输出,或next()一直done: true。
原因:遍历器内部指针是一次性的,已被消费到底。
修法:重新调用values()/keys()/entries()获取新遍历器,不要复用已耗尽的实例。现象:
for (const v of set.values())中把v当成{ value, done }。
原因:for...of取的是每次next().value,不是整个结果对象。
修法:手动next()时才判断done/value;for...of中直接处理值。
练习
- 定义
MyArray extends Array,构造时接收一个name和任意多个数字,实例既要保留数组特性,又要拥有name自有属性。 - 用
Set完成数组去重并返回新数组,分别用扩展运算符和Array.from实现;再验证对象元素为何不会按内容去重。 - 创建一个
Map,分别用数组、对象、Set、字符串、数字作键,演示set/get/has/delete;对比“同一对象引用”和“新建对象字面量”的has结果。 - 写函数
consume(iterable):手动调用iterator.next()收集值,遇到done停止;再用for...of写一版,并观察同一个 iterator 被消费两次的结果。 - 设计一个
WeakMap缓存:以 DOM 元素或普通对象为键存元数据,验证原始类型作键会报错,并说明为什么不能遍历它。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 继承内置类 | 07:27 |
| 2 | Set | 23:41 |
| 3 | WeakSet | 08:52 |
| 4 | Map | 16:27 |
| 5 | WeakMap | 04:14 |
| 6 | 遍历器对象 | 20:53 |
小结
- 继承内置类的关键是构造链:先
super再this,参数收集与展开必须成对处理。 Set是唯一值集合,不是索引数组;对象字面量不会因为“长得一样”而相等。Map的键可为任意类型,但对象键同样按引用比较,查询和修改必须使用同一引用。WeakSet/WeakMap的“弱”伴随限制:键或成员限对象、不可遍历、无size。- iterator 是一次性游标;
next()返回{ value, done },for...of本质是在消费这个游标。 - 需要重复遍历时,应重新获取 iterator;转数组可用扩展运算符或
Array.from,前提是可迭代。 - 选型结论:去重和成员判断优先
Set,复杂键映射优先Map,对象生命周期关联且无需枚举时用 Weak 系列。 - 排错先问三件事:按值还是比较引用、是否可迭代、iterator 是否已被消费。