21-Symbol、Map 与 Set
Symbol、Map 与 Set
三种 ES6 新增的数据类型,分别解决「唯一标识」「键值对映射」和「值去重」的场景,让代码表达更精准、性能更优。
学习目标
读完本文,你将学会:
- Symbol 的用途:创建唯一标识、定义常量、模拟私有属性
- Map 的用法:何时用 Map 替代 Object,常用 API 与遍历方式
- Set 的用法:去重、集合运算(交并差)
- WeakMap 与 WeakSet 的适用场景
一、Symbol:独一无二的标识符
1.1 基本用法
Symbol 是一种新的原始数据类型,每个 Symbol 值都是唯一的。
consts1=Symbol();consts2=Symbol();console.log(s1===s2);// false可以给 Symbol 添加描述(仅用于调试):
constid=Symbol("userId");console.log(id.description);// "userId"1.2 用作对象属性键
Symbol 可以作为对象属性键,不会与字符串键冲突:
constuser={name:"小明",[Symbol("id")]:123};// Symbol 属性不会出现在 for...in 中for(letkeyinuser){console.log(key);// 只输出 "name"}// 用 Object.keys 也拿不到console.log(Object.keys(user));// ["name"]// 需要用专门的 APIconsole.log(Object.getOwnPropertySymbols(user));// [Symbol(id)]1.3 定义常量,避免魔法字符串
// 传统方式:容易冲突、拼写错误constSTATUS_PENDING="pending";constSTATUS_DONE="done";// Symbol 方式:绝对唯一constSTATUS={PENDING:Symbol("pending"),DONE:Symbol("done"),ERROR:Symbol("error")};functionhandle(status){switch(status){caseSTATUS.PENDING:return"加载中...";caseSTATUS.DONE:return"完成!";caseSTATUS.ERROR:return"出错了";}}1.4 Symbol.for 与 Symbol.keyFor
Symbol.for()会在全局注册表中查找或创建 Symbol,相同 key 返回同一个 Symbol:
consta=Symbol.for("app.config");constb=Symbol.for("app.config");console.log(a===b);// true// 反向查找 keyconsole.log(Symbol.keyFor(a));// "app.config"// 普通 Symbol 不在全局注册表中constc=Symbol("local");console.log(Symbol.keyFor(c));// undefined1.5 常用内置 Symbol(Well-Known Symbols)
JavaScript 预定义了一些 Symbol,用于控制对象行为:
| Symbol | 作用 |
|---|---|
Symbol.iterator | 定义对象的默认迭代器(for…of) |
Symbol.toStringTag | 自定义Object.prototype.toString.call()的返回值 |
Symbol.hasInstance | 自定义instanceof行为 |
// Symbol.toStringTag 示例classMyClass{get[Symbol.toStringTag](){return"MyClass";}}console.log(Object.prototype.toString.call(newMyClass()));// "[object MyClass]"二、Map:比 Object 更强大的键值对
2.1 为什么需要 Map
Object 用作键值对有一些局限:
| Object 的局限 | Map 的优势 |
|---|---|
| 键只能是字符串或 Symbol | 键可以是任意类型(对象、函数、NaN 等) |
| 没有直接获取大小的方法 | 有.size属性 |
| 遍历顺序不保证 | 按插入顺序遍历 |
| 原型链可能带来意外属性 | 纯净,无原型链干扰 |
2.2 基本用法
constmap=newMap();// 设置键值对map.set("name","小明");map.set(123,"数字键");map.set({id:1},"对象键");// 获取值console.log(map.get("name"));// "小明"console.log(map.get(123));// "数字键"// 检查是否存在console.log(map.has("name"));// true// 删除map.delete(123);// 大小console.log(map.size);// 2// 清空map.clear();2.3 初始化与迭代
// 用数组初始化constuserMap=newMap([["name","小明"],["age",18],["city","北京"]]);// 遍历键值对for(const[key,value]ofuserMap){console.log(`${key}:${value}`);}// 只遍历键for(constkeyofuserMap.keys()){console.log(key);}// 只遍历值for(constvalueofuserMap.values()){console.log(value);}// forEachuserMap.forEach((value,key)=>{console.log(`${key}=${value}`);});2.4 Map 与 Object 互转
constmap=newMap([["a",1],["b",2]]);// Map → Objectconstobj=Object.fromEntries(map);console.log(obj);// { a: 1, b: 2 }// Object → Mapconstmap2=newMap(Object.entries(obj));console.log(map2);// Map { 'a' => 1, 'b' => 2 }2.5 实用场景
// 用对象做键(缓存场景)constcache=newMap();functionfetchData(user){if(cache.has(user)){returncache.get(user);// 命中缓存}constdata={/* 请求数据 */};cache.set(user,data);returndata;}constuser1={id:1};fetchData(user1);// 首次请求fetchData(user1);// 命中缓存三、Set:自动去重的值集合
3.1 基本用法
Set 是值的集合,每个值只能出现一次。
constset=newSet();set.add(1);set.add(2);set.add(2);// 重复,被忽略set.add(3);console.log(set.size);// 3console.log(set.has(2));// trueset.delete(2);console.log([...set]);// [1, 3]3.2 最常用:数组去重
constnumbers=[1,2,2,3,3,3,4];constunique=[...newSet(numbers)];console.log(unique);// [1, 2, 3, 4]// 字符串去重constchars=[...newSet("hello")];console.log(chars);// ['h', 'e', 'l', 'o']3.3 集合运算
Set 本身没有内置交并差方法,但可以用扩展运算符实现:
consta=newSet([1,2,3]);constb=newSet([2,3,4]);// 并集constunion=newSet([...a,...b]);console.log([...union]);// [1, 2, 3, 4]// 交集constintersection=newSet([...a].filter(x=>b.has(x)));console.log([...intersection]);// [2, 3]// 差集(a 有但 b 没有)constdifference=newSet([...a].filter(x=>!b.has(x)));console.log([...difference]);// [1]3.4 遍历 Set
constset=newSet(["red","green","blue"]);// for...offor(constcolorofset){console.log(color);}// forEach(注意:Set 的 forEach 参数是 value, value, set)set.forEach((value)=>{console.log(value);});四、WeakMap 与 WeakSet
4.1 WeakMap
WeakMap 与 Map 类似,但有两个关键区别:
- 键必须是对象(不能是原始值)
- 键是弱引用,不阻止垃圾回收
letuser={name:"小明"};constweakMap=newWeakMap();weakMap.set(user,"额外数据");// 当 user 不再被其他地方引用时user=null;// weakMap 中的条目会被自动回收用途:给对象附加私有数据,而不影响其生命周期。
constprivateData=newWeakMap();classUser{constructor(name){privateData.set(this,{password:"secret"});this.name=name;}getPassword(){returnprivateData.get(this).password;}}4.2 WeakSet
WeakSet 只能存储对象,同样是弱引用:
constvisited=newWeakSet();functionprocess(obj){if(visited.has(obj))return;// 已处理过visited.add(obj);// 处理逻辑...}WeakMap/WeakSet 的限制:
- 没有
.size属性 - 不能遍历(keys/values/entries/forEach 都没有)
- 不能清除(没有
.clear())
五、Map vs Object,Set vs Array
Map vs Object 怎么选?
| 场景 | 推荐 |
|---|---|
| 键需要是对象/函数 | Map |
| 频繁增删键值对 | Map |
| 需要保持插入顺序 | Map |
| 需要知道大小 | Map |
| 简单的配置对象、JSON 数据 | Object |
| 需要 JSON 序列化 | Object(Map 不能直接 JSON.stringify) |
Set vs Array 怎么选?
| 场景 | 推荐 |
|---|---|
| 需要去重 | Set |
| 频繁检查元素是否存在 | Set(O(1)) |
| 需要有序列表、按索引访问 | Array |
| 需要 map/filter/reduce | Array |
六、常见误区与注意点
| 误区 | 正确理解 |
|---|---|
| Symbol 可以用 new 创建 | new Symbol()会报错,用Symbol() |
Symbol.for("x") === Symbol("x") | Symbol.for和Symbol创建的是不同的 Symbol |
| Set 去重对对象有效 | {a:1}和{a:1}是不同的对象,都能放入 Set |
Map 可以用.语法访问 | Map 用.get()和.set(),不是.map.key |
| WeakMap 可以遍历 | WeakMap 没有迭代方法,不可遍历 |
typeof Symbol()是 “object” | typeof Symbol()是 “symbol” |
Symbol 属性不可枚举的完整含义
constobj={name:"小明",[Symbol("id")]:123};console.log(JSON.stringify(obj));// '{"name":"小明"}'console.log(Object.keys(obj));// ["name"]console.log(Object.getOwnPropertySymbols(obj));// [Symbol(id)]七、动手练习
练习 1:用 Symbol 实现枚举
用 Symbol 定义一组颜色常量,避免字符串常量冲突:
constCOLOR={/* ... */};functiongetColorHex(color){// 返回对应颜色的十六进制值}参考答案constCOLOR={RED:Symbol("red"),GREEN:Symbol("green"),BLUE:Symbol("blue")};functiongetColorHex(color){constmap={[COLOR.RED]:"#ff0000",[COLOR.GREEN]:"#00ff00",[COLOR.BLUE]:"#0000ff"};returnmap[color]||"#000000";}console.log(getColorHex(COLOR.RED));// "#ff0000"练习 2:Map 统计字符频率
输入一个字符串,用 Map 统计每个字符出现的次数:
functioncountChars(str){// 返回一个 Map,key 是字符,value 是次数}console.log(countChars("hello"));// Map { 'h' => 1, 'e' => 1, 'l' => 2, 'o' => 1 }参考答案functioncountChars(str){constmap=newMap();for(constcharofstr){map.set(char,(map.get(char)||0)+1);}returnmap;}练习 3:Set 实现数组交集
写一个不依赖扩展运算符的数组交集函数:
functionintersection(arr1,arr2){// 返回两个数组的交集(去重)}console.log(intersection([1,2,2,3],[2,3,4]));// [2, 3]参考答案functionintersection(arr1,arr2){constset2=newSet(arr2);constresult=newSet();for(constitemofarr1){if(set2.has(item))result.add(item);}return[...result];}八、AI 辅助学习
8.1 本节知识点的 AI 提问模板
【背景】我是 JavaScript 初学者,正在学习第 21 篇"Symbol、Map 与 Set"。 我已经了解 Object 和 Array 的基本用法。 【问题】我理解 Map 可以替代 Object 做键值对存储,但我不太清楚什么时候 应该优先选择 Map。在 React/Vue 这样的框架中,哪些场景更适合用 Map? 【期望】请对比 Map 和 Object 在以下场景的表现:键类型多样性、迭代性能、 内存占用、JSON 序列化。给出 2 个前端开发中适合用 Map 的具体例子。8.2 用 AI 验证你的理解
- 问 AI:“
typeof Symbol('x')的结果是什么?typeof new Map()呢?” - 让 AI 解释:“为什么 WeakMap 的键必须是对象?”
- 让 AI 出题:“写一道 Set 去重和 filter 去重的性能对比题”
8.3 警惕 AI 的常见错误
- AI 可能写出
new Symbol()(正确是Symbol()) - AI 可能声称 Map 的键是无序的(实际按插入顺序)
- AI 可能在 WeakMap 中使用原始值作为键(会报错)
- AI 可能混淆
Symbol.for和Symbol的区别
九、配套代码
本文示例代码位于:CODE/21-Symbol-Map-Set/
| 文件名 | 说明 |
|---|---|
data-structures-lab.html | Symbol、Map、Set 交互式实验室 |
十、本章小结
- Symbol:唯一标识符,适合枚举、私有属性,用
Symbol.for共享 - Map:键可为任意类型,按插入顺序,有
.size,支持任意键对象 - Set:值不重复,最常用场景是数组/字符串去重
- WeakMap/WeakSet:弱引用,键必须是对象,不可遍历,适合附加元数据
- 选择建议:需要任意键用 Map,需要去重用 Set,需要不影响 GC 用 WeakMap
十一、下篇预告
下一篇学习面向对象新语法:《类(Class):面向对象的新写法》,你将学到:
- class 语法糖与构造函数的对比
- 继承 extends 和方法重写
- 静态属性和私有字段
- getter/setter 的优雅写法
如果本文对你有帮助,欢迎点赞、收藏、关注专栏。有任何问题可以在评论区交流!
