ES6 引入了第七种原始数据类型 Symbol,用于创建唯一的标识符。
为什么需要 Symbol#
在 JavaScript 中,对象属性名只能是字符串,这会导致命名冲突:
// 两个库都想给对象添加 id 属性const obj = {}library1.addId(obj) // obj.id = 'lib1-123'library2.addId(obj) // obj.id = 'lib2-456'(覆盖了!)
// 使用 Symbol 可以避免冲突const id1 = Symbol('id')const id2 = Symbol('id')obj[id1] = 'lib1-123'obj[id2] = 'lib2-456'// 两个属性互不影响创建 Symbol#
基本创建#
const sym1 = Symbol()const sym2 = Symbol()console.log(sym1 === sym2) // false(每个 Symbol 都是唯一的)
// 带描述符const sym3 = Symbol('description')console.log(sym3.toString()) // 'Symbol(description)'console.log(sym3.description) // 'description'🔶 不能使用 new#
// new Symbol(); // TypeError: Symbol is not a constructor描述符只是备注#
const sym1 = Symbol('id')const sym2 = Symbol('id')console.log(sym1 === sym2) // false(即使描述符相同)作为属性键#
定义 Symbol 属性#
const ID = Symbol('id')const NAME = Symbol('name')
// 方式1:对象字面量const user = { [ID]: 1, [NAME]: '张三', age: 25,}
// 方式2:点号不行,必须用方括号user[ID] // 1// user.ID; // undefined(这是访问字符串键 'ID')
// 方式3:Object.definePropertyObject.defineProperty(user, Symbol('email'), { value: 'test@test.com',})Symbol 属性不可枚举#
const ID = Symbol('id')const user = { [ID]: 1, name: '张三', age: 25,}
// for...in 不会遍历 Symbol 属性for (const key in user) { console.log(key) // 'name', 'age'(没有 Symbol)}
// Object.keys 也不包含Object.keys(user) // ['name', 'age']
// Object.getOwnPropertyNames 也不包含Object.getOwnPropertyNames(user) // ['name', 'age']
// 需要使用专门的方法Object.getOwnPropertySymbols(user) // [Symbol(id)]
// 或者 Reflect.ownKeys 获取所有键Reflect.ownKeys(user) // ['name', 'age', Symbol(id)]实际应用#
私有属性模拟#
const _count = Symbol('count')const _increment = Symbol('increment')
class Counter { constructor() { this[_count] = 0 }
[_increment]() { this[_count]++ }
tick() { this[_increment]() }
get value() { return this[_count] }}
const counter = new Counter()counter.tick()counter.tick()console.log(counter.value) // 2console.log(counter[_count]) // 如果能访问到 Symbol,仍然可以读取// 但外部代码通常无法获取这个 Symbol防止属性覆盖#
// 框架定义的属性const INTERNAL = Symbol('internal')
function createComponent(options) { const component = { ...options, [INTERNAL]: { mounted: false, listeners: [], }, } return component}
// 用户不会意外覆盖内部属性const comp = createComponent({ name: 'MyComponent', internal: 'user data', // 不会与 INTERNAL 冲突})常量定义#
// 传统方式可能会冲突const STATUS_PENDING = 'pending'const STATUS_SUCCESS = 'success'const STATUS_ERROR = 'error'
// 如果其他地方也定义了 'pending',比较时会相等
// 使用 Symbol 确保唯一const STATUS = { PENDING: Symbol('pending'), SUCCESS: Symbol('success'), ERROR: Symbol('error'),}
function handleStatus(status) { switch (status) { case STATUS.PENDING: return '处理中...' case STATUS.SUCCESS: return '成功' case STATUS.ERROR: return '错误' default: throw new Error('未知状态') }}消除魔术字符串#
// 魔术字符串function getArea(shape, options) { switch (shape) { case 'circle': // 魔术字符串 return Math.PI * options.radius ** 2 case 'rectangle': return options.width * options.height }}
// 使用 Symbolconst SHAPES = { CIRCLE: Symbol('circle'), RECTANGLE: Symbol('rectangle'),}
function getArea(shape, options) { switch (shape) { case SHAPES.CIRCLE: return Math.PI * options.radius ** 2 case SHAPES.RECTANGLE: return options.width * options.height }}
getArea(SHAPES.CIRCLE, { radius: 5 })类型转换#
转字符串#
const sym = Symbol('foo')
String(sym) // 'Symbol(foo)'sym.toString() // 'Symbol(foo)'
// 不能隐式转换// 'symbol: ' + sym; // TypeError// `symbol: ${sym}`; // TypeError
// 必须显式转换;`symbol: ${String(sym)}` // 'symbol: Symbol(foo)';`symbol: ${sym.toString()}` // 'symbol: Symbol(foo)'转布尔值#
const sym = Symbol()Boolean(sym) // true!sym // false
if (sym) { console.log('Symbol 是真值')}🔶 不能转数字#
const sym = Symbol()// Number(sym); // TypeError// sym + 1; // TypeErrorJSON 序列化#
Symbol 属性不会被 JSON.stringify 序列化:
const ID = Symbol('id')const user = { [ID]: 1, name: '张三',}
JSON.stringify(user) // '{"name":"张三"}'(没有 Symbol 属性)常见问题#
🙋 Symbol 和字符串有什么区别?#
| 特性 | Symbol | 字符串 |
|---|---|---|
| 唯一性 | 每个 Symbol 唯一 | 相同字符串相等 |
| 创建 | Symbol() | ’…’ 或 new String() |
| 类型 | symbol | string |
| 可枚举性 | 默认不可枚举 | 默认可枚举 |
| JSON | 不序列化 | 正常序列化 |
🙋 什么时候用 Symbol?#
- 需要唯一标识符时
- 定义对象内部属性,不希望被意外访问或覆盖
- 定义常量,避免魔术字符串
- 实现特殊行为(内置 Symbol)
🙋 Symbol 是真正的私有属性吗?#
不是。Symbol 属性可以通过 Object.getOwnPropertySymbols() 或 Reflect.ownKeys() 获取。它只是”隐藏”了属性,但没有真正的访问控制。
真正的私有属性应该使用 ES2022 的 # 语法:
class Counter { #count = 0 // 真正的私有属性
increment() { this.#count++ }}小结#
| 特性 | 说明 |
|---|---|
| 唯一性 | 每个 Symbol 都是唯一的 |
| 创建 | Symbol() 或 Symbol(‘description’) |
| 属性键 | 只能用方括号语法 obj[sym] |
| 可枚举 | 不被 for…in、Object.keys 等遍历 |
| 获取 | Object.getOwnPropertySymbols() |
| 类型转换 | 可转字符串和布尔值,不能转数字 |
Symbol 为 JavaScript 提供了创建唯一标识符的能力,在框架开发和元编程中非常有用。