ES6 Symbol

138 阅读4分钟

一、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

实现私有属性的两种方式对比:

特性SymbolWeakMap
可枚举性可枚举性可控完全不可枚举
内存管理需手动管理自动垃圾回收
访问控制需要持有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. 使用建议

  1. 合理使用:不要过度使用Symbol,普通字符串属性能满足需求时优先使用字符串
  2. 文档说明:对使用的Symbol属性进行充分文档说明
  3. 全局Symbol:跨模块共享Symbol时使用Symbol.for()
  4. 内置Symbol:优先使用语言提供的内置Symbol值
  5. 私有性:ES2022+环境中,真正的私有字段(#)比Symbol更适合实现私有成员

3. 性能考量

  • Symbol属性访问速度与字符串属性相当
  • Object.getOwnPropertySymbols()比Object.keys()稍慢
  • 全局Symbol注册表有轻微性能开销