一、Symbol 基本概念
1. 什么是 Symbol
Symbol 是 ES6 引入的一种原始数据类型,表示唯一的值。它主要用于创建对象的唯一属性名,避免属性名冲突。
// 创建 Symbol
const sym1 = Symbol();
const sym2 = Symbol('description'); // 可选的描述字符串
console.log(typeof sym1); // "symbol"
console.log(sym2.toString()); // "Symbol(description)"
2. 核心特性
- 唯一性:每个 Symbol 值都是唯一的
- 不可变性:Symbol 值创建后不能被修改
- 非字符串属性名:可以作为对象属性的键
const sym1 = Symbol('key');
const sym2 = Symbol('key');
console.log(sym1 === sym2); // false,即使描述相同也不相等
二、Symbol 的创建与使用
1. 创建 Symbol
// 不带描述的 Symbol
const anonymousSymbol = Symbol();
// 带描述的 Symbol (仅用于调试,不影响唯一性)
const namedSymbol = Symbol('mySymbol');
// 全局 Symbol 注册表
const globalSym = Symbol.for('globalKey'); // 不存在则创建,存在则返回
2. 作为对象属性
const obj = {};
const sym = Symbol('uniqueKey');
// 作为属性名
obj[sym] = 'private value';
// 字面量写法
const myObj = {
[sym]: 'value'
};
console.log(obj[sym]); // "private value"
3. 属性遍历
Symbol 属性不会出现在常规遍历中:
const obj = {
[Symbol('key1')]: 'value1',
regularKey: 'value2'
};
// 不会出现在 for...in 循环中
for (const key in obj) {
console.log(key); // 只输出 "regularKey"
}
// 不会出现在 Object.keys() 中
console.log(Object.keys(obj)); // ["regularKey"]
// 需要使用 Object.getOwnPropertySymbols() 获取
const symbolKeys = Object.getOwnPropertySymbols(obj);
console.log(symbolKeys); // [Symbol(key1)]
三、Symbol 的静态方法与属性
1. Symbol.for(key)
在全局 Symbol 注册表中查找或创建 Symbol:
const globalSym1 = Symbol.for('app.key');
const globalSym2 = Symbol.for('app.key');
console.log(globalSym1 === globalSym2); // true
2. Symbol.keyFor(sym)
返回全局 Symbol 的描述:
const globalSym = Symbol.for('app.key');
console.log(Symbol.keyFor(globalSym)); // "app.key"
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined
3. 内置 Symbol 值
ES6 提供了一系列内置 Symbol 值,用于改变语言内部行为:
| Symbol 属性 | 用途 |
|---|---|
| Symbol.iterator | 定义对象的默认迭代器 |
| Symbol.asyncIterator | 定义异步迭代器 |
| Symbol.hasInstance | 自定义 instanceof 行为 |
| Symbol.toStringTag | 定义 Object.prototype.toString() 返回值 |
| Symbol.species | 创建派生对象时使用的构造函数 |
| Symbol.toPrimitive | 定义对象转换为原始值的行为 |
四、内置 Symbol 的实用示例
1. Symbol.iterator
使对象可迭代:
const myIterable = {
[Symbol.iterator]: function* () {
yield 1;
yield 2;
yield 3;
}
};
console.log([...myIterable]); // [1, 2, 3]
2. Symbol.toStringTag
自定义对象类型标签:
class MyClass {
get [Symbol.toStringTag]() {
return 'MyCustomClass';
}
}
const instance = new MyClass();
console.log(instance.toString()); // "[object MyCustomClass]"
3. Symbol.toPrimitive
控制对象转换为原始值:
const temperature = {
value: 20,
[Symbol.toPrimitive](hint) {
if (hint === 'string') return `${this.value}°C`;
return this.value; // 默认返回数字
}
};
console.log(String(temperature)); // "20°C"
console.log(temperature + 5); // 25
五、Symbol 的高级应用
1. 实现私有属性(ES2022+)
结合私有字段和 Symbol 实现更严格的私有性:
const _counter = Symbol('counter');
const _action = Symbol('action');
class Countdown {
constructor(counter, action) {
this[_counter] = counter;
this[_action] = action;
}
dec() {
if (this[_counter] < 1) return;
this[_counter]--;
if (this[_counter] === 0) {
this[_action]();
}
}
}
const cd = new Countdown(3, () => console.log('DONE'));
cd.dec(); cd.dec(); cd.dec(); // 输出 "DONE"
2. 元编程
使用 Symbol 改变语言内部行为:
class MyArray extends Array {
static get [Symbol.species]() { return Array; }
}
const myArr = new MyArray(1, 2, 3);
const mapped = myArr.map(x => x * 2);
console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array); // true
3. 协议实现
实现各种语言协议:
// 实现异步迭代协议
const asyncIterable = {
[Symbol.asyncIterator]: async function* () {
for (let i = 0; i < 3; i++) {
await new Promise(resolve => setTimeout(resolve, 100));
yield i;
}
}
};
(async () => {
for await (const num of asyncIterable) {
console.log(num); // 0, 1, 2 (间隔100ms)
}
})();
六、Symbol 的注意事项
1. 类型转换
Symbol 不能隐式转换为字符串或数字:
const sym = Symbol('desc');
console.log(String(sym)); // "Symbol(desc)" (显式转换允许)
console.log(sym + ''); // TypeError
console.log(+sym); // TypeError
2. JSON 序列化
Symbol 属性会被 JSON.stringify() 忽略:
const obj = {
regular: 'value',
[Symbol('key')]: 'symbolValue'
};
console.log(JSON.stringify(obj)); // {"regular":"value"}
3. 与 Reflect.ownKeys()
Reflect.ownKeys() 可以获取所有键(包括 Symbol):
const obj = {
[Symbol('key1')]: 1,
key2: 2
};
console.log(Reflect.ownKeys(obj)); // ["key2", Symbol(key1)]
七、Symbol 实际应用场景
1. 防止属性冲突
在扩展对象时避免命名冲突:
// 第三方库可能使用的属性名
const LIB_PREFIX = 'myLib_';
const internalKey = Symbol(LIB_PREFIX + 'internalData');
function myLib(obj) {
obj[internalKey] = { initialized: true };
// ...其他实现
}
2. 定义特殊行为
自定义对象在特定场景的行为:
const loggable = {
[Symbol.toPrimitive](hint) {
console.log(`Converting with hint: ${hint}`);
return hint === 'string' ? 'loggable' : 0;
}
};
console.log(`${loggable}`); // 输出提示后返回 "loggable"
console.log(+loggable); // 输出提示后返回 0
3. 实现接口/协议
定义可被识别的接口:
// 定义可渲染接口
const RENDERABLE = Symbol('renderable');
class Widget {
[RENDERABLE]() {
console.log('Rendering widget...');
}
}
function render(component) {
if (component[RENDERABLE]) {
component[RENDERABLE]();
}
}
const widget = new Widget();
render(widget); // "Rendering widget..."
八、Symbol 与相关技术对比
1. Symbol vs WeakMap
实现私有属性的两种方式对比:
| 特性 | Symbol | WeakMap |
|---|---|---|
| 可枚举性 | 可枚举性可控 | 完全不可枚举 |
| 内存管理 | 需手动管理 | 自动垃圾回收 |
| 访问控制 | 需要持有Symbol引用 | 需要持有WeakMap实例引用 |
| 性能 | 访问更快 | 访问稍慢 |
2. Symbol vs 字符串常量
作为属性名的两种选择:
// 使用字符串常量
const LOG_LEVEL = {
DEBUG: 'DEBUG',
INFO: 'INFO'
};
// 使用Symbol
const LOG_LEVEL = {
DEBUG: Symbol('DEBUG'),
INFO: Symbol('INFO')
};
// Symbol优势:保证唯一性,不会被意外覆盖
九、最新规范中的 Symbol(ES2023+)
1. Symbol 作为 WeakMap 键(ES2023)
const weakMap = new WeakMap();
const key = Symbol('privateData');
const obj = {};
weakMap.set(key, 'symbolValue');
weakMap.set(obj, 'objectValue');
console.log(weakMap.get(key)); // "symbolValue"
console.log(weakMap.get(obj)); // "objectValue"
2. 新增内置 Symbol
未来可能新增的内置 Symbol 值:
Symbol.dispose:用于显式资源管理Symbol.metadata:用于装饰器元数据
// 提案阶段的Symbol.dispose
{
const resource = {
[Symbol.dispose]: () => console.log('Disposed!')
};
using res = resource; // 离开块作用域时自动调用dispose
} // 输出 "Disposed!"
十、总结与最佳实践
1. Symbol 的核心价值
- 唯一性:创建不会冲突的标识符
- 元编程:改变语言内部行为
- 协议实现:实现迭代器等各种协议
- 弱封装:实现伪私有属性(配合约定)
2. 使用建议
- 合理使用:不要过度使用Symbol,普通字符串属性能满足需求时优先使用字符串
- 文档说明:对使用的Symbol属性进行充分文档说明
- 全局Symbol:跨模块共享Symbol时使用Symbol.for()
- 内置Symbol:优先使用语言提供的内置Symbol值
- 私有性:ES2022+环境中,真正的私有字段(#)比Symbol更适合实现私有成员
3. 性能考量
- Symbol属性访问速度与字符串属性相当
- Object.getOwnPropertySymbols()比Object.keys()稍慢
- 全局Symbol注册表有轻微性能开销