Symbol 有什么用?常见内置 Symbol 有哪些?
简化版
Symbol 是 ES6 新增的原始类型,用来创建唯一值,常用于避免对象属性名冲突。内置 Symbol 如 Symbol.iterator、Symbol.toStringTag 可以定制对象的语言行为。
详细版
基本用法:
const key = Symbol('id');
const obj = {
[key]: 123,
};
每个 Symbol 都是唯一的:
Symbol('a') === Symbol('a'); // false
常见内置 Symbol:
Symbol.iterator:定义对象如何被迭代。Symbol.toStringTag:定制Object.prototype.toString结果。Symbol.hasInstance:定制instanceof行为。
Symbol 属性不会被普通 for...in、Object.keys 枚举到,但可以用 Object.getOwnPropertySymbols 获取。
完整版教学
一、Symbol 解决属性名冲突
如果多个库都往同一个对象上挂属性,字符串 key 可能冲突。Symbol 每次创建都唯一,可以降低冲突概率。
const PRIVATE = Symbol('private');
obj[PRIVATE] = 'secret';
它不是绝对私有,只是不容易被普通枚举拿到。
二、全局 Symbol 注册表
Symbol.for() 会在全局注册表里查找或创建 Symbol:
Symbol.for('app') === Symbol.for('app'); // true
它适合跨模块共享同一个 Symbol。普通 Symbol() 每次都是新的。
三、Symbol.iterator 和可迭代对象
对象实现 Symbol.iterator 后,就可以被 for...of、展开运算符消费:
const range = {
*[Symbol.iterator]() {
yield 1;
yield 2;
},
};
这说明 Symbol 不只是唯一 key,还参与了 JS 的协议设计。
四、Symbol 属性的枚举特点
Symbol key 不会出现在 Object.keys 和 JSON 序列化中。如果要获取:
Object.getOwnPropertySymbols(obj);
Reflect.ownKeys(obj);
Reflect.ownKeys 会同时拿到字符串 key 和 Symbol key。
五、面试追问与工程落地
Symbol 常见追问是“Symbol 能不能保证私有”。它不能保证真正私有,因为可以通过 Object.getOwnPropertySymbols 或 Reflect.ownKeys 拿到 Symbol 属性。它更多是避免命名冲突和普通枚举,不是安全边界。
还会问 Symbol.iterator 为什么重要。它定义了对象如何被 for...of、展开运算符、Array.from 消费。数组、字符串、Map、Set 都实现了这个协议,所以它们能被统一迭代。
工程里 Symbol 适合做库内部扩展点或元信息 key,减少和用户字段冲突。但业务数据字段一般不要用 Symbol,因为 JSON 序列化、接口传输、调试展示都不方便。
六、从唯一性、注册表和协议三个层次理解
每次调用 Symbol('id') 都创建新的 Symbol,描述字符串只用于调试,不参与相等判断;Symbol.for('id') 则查询全局 Symbol 注册表,相同键会取得同一个 Symbol。反向查询仅适用于注册表中的 Symbol,可用 Symbol.keyFor 获得注册键。
const localA = Symbol('id')
const localB = Symbol('id')
const sharedA = Symbol.for('id')
const sharedB = Symbol.for('id')
console.log(localA === localB) // false
console.log(sharedA === sharedB) // true
console.log(Symbol.keyFor(localA)) // undefined
| 创建方式 | 相同描述或键是否相等 | Symbol.keyFor |
|---|---|---|
Symbol('x') | 否 | undefined |
Symbol.for('x') | 是 | "x" |
Symbol.iterator | 固定的内建 Symbol | 不属于注册表 |
Well-known Symbols 是语言预留的扩展协议。例如对象通过 [Symbol.iterator] 接入迭代协议,通过 [Symbol.toPrimitive] 控制原始值转换;它们的价值不是“隐藏字段”,而是避免协议名称与普通业务属性冲突。
枚举时要区分 API:Object.keys、for...in 和 JSON.stringify 不处理 Symbol 键,Object.getOwnPropertySymbols 可单独取得,Reflect.ownKeys 则同时返回字符串键与 Symbol 键。假设对象有 2 个字符串键和 1 个 Symbol 键,Object.keys 长度是 2,而 Reflect.ownKeys 长度是 3。
Symbol 属性只是较不容易意外碰撞或被常规枚举发现,并不提供安全边界;拿到对象的人仍可通过反射 API 读取它们。
七、常见误区与追问
- 误区:相同描述的 Symbol 一定相等。
Symbol('x')每次都创建唯一值,描述不是身份标识。 - 误区:Symbol 属性是真正私有属性。
Object.getOwnPropertySymbols和Reflect.ownKeys都能发现它,私有字段应使用#field。 - 误区:Symbol 键不会被任何复制操作带走。 对象展开和
Object.assign会复制自有可枚举 Symbol 属性。 - 追问:Symbol.for 适合解决什么问题? 它让约定相同注册键的代码取得同一个 Symbol,适合跨模块共享标识。
- 追问:为什么不能 new Symbol? Symbol 是原始值工厂,不是可构造函数,配合
new会抛出TypeError。 - 追问:JSON.stringify 如何处理 Symbol? 对象的 Symbol 键会被忽略,数组中的 Symbol 值通常序列化为
null,对象属性值为 Symbol 时该属性被忽略。 - 追问:well-known Symbol 和 Symbol.for 有什么关系? 前者由语言规范定义并驱动内建协议,后者来自可按字符串键查询的全局注册表,两者用途不同。
八、加强记忆
Symbol 记成“唯一标识符”。普通 Symbol 每次不同,Symbol.for 可共享;它既能防属性冲突,也能通过内置 Symbol 接入迭代等语言协议。
最后用唯一 Symbol、注册表 Symbol、well-known Symbol 三类分别回答身份共享、跨模块约定和语言协议问题。