返回首页

28. 说说你对Symbol的理解?

问题解析

Symbol 是 ES6 引入的一种新的原始数据类型,表示独一无二的值。面试中主要考察 Symbol 的基本用法、唯一性特点、作为对象属性的应用以及内置 Symbol 的使用。

核心概念

1. 什么是 Symbol

Symbol 是 JavaScript 的第七种原始数据类型(前六种是 string、number、boolean、null、undefined、bigint),每个 Symbol 值都是唯一的。

// 创建 Symbol
const sym1 = Symbol();
const sym2 = Symbol('description');
const sym3 = Symbol('description');

// 每个 Symbol 都是唯一的
console.log(sym2 === sym3); // false

// typeof 返回 'symbol'
console.log(typeof sym1); // 'symbol'

2. Symbol 的特点

// 1. 不能 new
// const sym = new Symbol(); // TypeError: Symbol is not a constructor

// 2. 不能与其他类型运算
const sym = Symbol('test');
// console.log(sym + 'string'); // TypeError
// console.log(sym + 1); // TypeError

// 3. 可以转换为字符串
console.log(sym.toString()); // 'Symbol(test)'
console.log(String(sym));    // 'Symbol(test)'

// 4. 可以转换为布尔值
console.log(Boolean(sym));   // true
// console.log(!sym);        // false

// 5. 不能转换为数字
// console.log(Number(sym)); // TypeError

详细解答

1. Symbol 的创建方式

// 方式1:Symbol() 函数
const sym1 = Symbol();
const sym2 = Symbol('description');

// 方式2:Symbol.for() - 全局注册表
const sym3 = Symbol.for('key');
const sym4 = Symbol.for('key');
console.log(sym3 === sym4); // true

// Symbol.keyFor() - 获取全局 Symbol 的 key
console.log(Symbol.keyFor(sym3)); // 'key'
console.log(Symbol.keyFor(sym1)); // undefined

// 方式3:内置 Symbol 常量
const obj = {
  [Symbol.iterator]: function() {
    // 实现迭代器
  }
};

2. 作为对象属性

// 作为对象属性名
const name = Symbol('name');
const age = Symbol('age');

const person = {
  [name]: 'Alice',
  [age]: 25,
  [Symbol('id')]: '12345', // 每次创建都是新的 Symbol
  regularProp: 'regular'
};

// 访问 Symbol 属性
console.log(person[name]);    // 'Alice'
console.log(person[age]);     // 25

// Symbol 属性不会被常规遍历
for (const key in person) {
  console.log(key); // 只输出 'regularProp'
}

console.log(Object.keys(person));        // ['regularProp']
console.log(Object.getOwnPropertyNames(person)); // ['regularProp']

// 获取 Symbol 属性的方法
console.log(Object.getOwnPropertySymbols(person));
// [Symbol(name), Symbol(age), Symbol(id)]

console.log(Reflect.ownKeys(person));
// ['regularProp', Symbol(name), Symbol(age), Symbol(id)]

3. Symbol 属性的特性

const sym = Symbol('hidden');
const obj = {
  [sym]: 'secret value',
  public: 'public value'
};

// Symbol 属性是枚举的,但不会被 for...in 遍历
console.log(Object.getOwnPropertyDescriptor(obj, sym));
// { value: 'secret value', writable: true, enumerable: true, configurable: true }

// JSON.stringify 会忽略 Symbol 属性
console.log(JSON.stringify(obj)); // '{"public":"public value"}'

// Object.assign 会复制 Symbol 属性
const copy = Object.assign({}, obj);
console.log(copy[sym]); // 'secret value'

深入理解

1. 消除魔术字符串

// ❌ 魔术字符串
function getArea(shape) {
  switch (shape) {
    case 'Triangle': return '三角形';
    case 'Circle': return '圆形';
    default: return '未知';
  }
}

// ✅ 使用 Symbol
const SHAPE_TRIANGLE = Symbol('triangle');
const SHAPE_CIRCLE = Symbol('circle');
const SHAPE_RECTANGLE = Symbol('rectangle');

function getAreaBetter(shape) {
  switch (shape) {
    case SHAPE_TRIANGLE: return '三角形';
    case SHAPE_CIRCLE: return '圆形';
    case SHAPE_RECTANGLE: return '矩形';
    default: return '未知';
  }
}

// 使用
console.log(getAreaBetter(SHAPE_TRIANGLE)); // '三角形'
// 不会与其他值冲突

2. 实现私有属性

const _name = Symbol('name');
const _age = Symbol('age');

class Person {
  constructor(name, age) {
    this[_name] = name;
    this[_age] = age;
  }

  getName() {
    return this[_name];
  }

  getAge() {
    return this[_age];
  }

  setAge(age) {
    this[_age] = age;
  }
}

const person = new Person('Alice', 25);
console.log(person.getName()); // 'Alice'

// 无法直接访问(虽然可以通过 Object.getOwnPropertySymbols 获取)
console.log(person[_name]); // 'Alice'
// 但外部不容易知道 Symbol 的引用

3. 内置 Symbol 常量

// 1. Symbol.iterator - 定义对象的默认迭代器
const iterable = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    const data = this.data;
    return {
      next() {
        if (index < data.length) {
          return { value: data[index++], done: false };
        }
        return { done: true };
      }
    };
  }
};

for (const item of iterable) {
  console.log(item); // 1, 2, 3
}

// 2. Symbol.toStringTag - 自定义对象的 toString 标签
class MyClass {
  get [Symbol.toStringTag]() {
    return 'MyClass';
  }
}

console.log(Object.prototype.toString.call(new MyClass()));
// '[object MyClass]'

// 3. Symbol.hasInstance - 自定义 instanceof 行为
class MyArray {
  static [Symbol.hasInstance](instance) {
    return Array.isArray(instance);
  }
}

console.log([] instanceof MyArray); // true

// 4. Symbol.toPrimitive - 自定义对象转换为原始值
const obj = {
  [Symbol.toPrimitive](hint) {
    if (hint === 'number') return 42;
    if (hint === 'string') return 'hello';
    return true;
  }
};

console.log(+obj);      // 42
console.log(`${obj}`);  // 'hello'
console.log(obj + '');  // 'true'

// 5. Symbol.species - 指定派生对象的构造函数
class MyArray2 extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

const arr = new MyArray2(1, 2, 3);
const mapped = arr.map(x => x * 2);
console.log(mapped instanceof MyArray2); // false
console.log(mapped instanceof Array);    // true

// 6. Symbol.match/replace/search/split - 自定义字符串方法
class MyMatcher {
  [Symbol.match](string) {
    return string.includes('test');
  }
}

console.log('testing'.match(new MyMatcher())); // true

最佳实践

1. 常量定义

// 使用 Symbol 定义常量,确保唯一性
const LOG_LEVEL = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn'),
  ERROR: Symbol('error')
};

function log(level, message) {
  switch (level) {
    case LOG_LEVEL.DEBUG:
      console.debug(`[DEBUG] ${message}`);
      break;
    case LOG_LEVEL.INFO:
      console.info(`[INFO] ${message}`);
      break;
    case LOG_LEVEL.WARN:
      console.warn(`[WARN] ${message}`);
      break;
    case LOG_LEVEL.ERROR:
      console.error(`[ERROR] ${message}`);
      break;
  }
}

// 使用
log(LOG_LEVEL.INFO, 'Application started');

2. 元数据存储

// 使用 Symbol 存储元数据,避免命名冲突
const metadataKey = Symbol('metadata');

function addMetadata(target, data) {
  if (!target[metadataKey]) {
    target[metadataKey] = {};
  }
  Object.assign(target[metadataKey], data);
}

function getMetadata(target) {
  return target[metadataKey] || {};
}

// 使用
const myFunction = () => {};
addMetadata(myFunction, { author: 'Alice', version: '1.0' });
console.log(getMetadata(myFunction)); // { author: 'Alice', version: '1.0' }

3. 框架/库的内部属性

// 框架可以使用 Symbol 定义内部属性,避免与用户属性冲突
const _listeners = Symbol('listeners');
const _state = Symbol('state');

class EventEmitter {
  constructor() {
    this[_listeners] = new Map();
    this[_state] = {};
  }

  on(event, handler) {
    if (!this[_listeners].has(event)) {
      this[_listeners].set(event, []);
    }
    this[_listeners].get(event).push(handler);
  }

  emit(event, data) {
    const handlers = this[_listeners].get(event) || [];
    handlers.forEach(handler => handler(data));
  }
}

4. 实现迭代器

// 为自定义对象实现迭代器
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { done: true };
      }
    };
  }
}

const range = new Range(1, 5);
console.log([...range]); // [1, 2, 3, 4, 5]

面试要点

  1. 唯一性:每个 Symbol 都是唯一的,即使描述相同
  2. 原始类型:Symbol 是原始数据类型,不是对象
  3. 不可枚举:Symbol 属性不会出现在 for...in、Object.keys() 中
  4. 应用场景:私有属性、常量定义、元数据存储、消除魔术字符串
  5. 内置 Symbol:用于自定义对象行为(iterator、toStringTag 等)

常见面试题

// 面试题 1:Symbol 和 Symbol.for 的区别?
const sym1 = Symbol('key');
const sym2 = Symbol('key');
const sym3 = Symbol.for('key');
const sym4 = Symbol.for('key');

console.log(sym1 === sym2); // false
console.log(sym3 === sym4); // true
console.log(sym1 === sym3); // false

// Symbol.for 在全局注册表中查找或创建
// Symbol 每次都创建新的

// 面试题 2:如何获取对象的所有 Symbol 属性?
const obj = {
  [Symbol('a')]: 1,
  [Symbol('b')]: 2,
  regular: 3
};

console.log(Object.getOwnPropertySymbols(obj));
// [Symbol(a), Symbol(b)]

console.log(Reflect.ownKeys(obj));
// ['regular', Symbol(a), Symbol(b)]

// 面试题 3:Symbol 属性会被 JSON.stringify 序列化吗?
const obj2 = {
  [Symbol('hidden')]: 'secret',
  visible: 'public'
};

console.log(JSON.stringify(obj2)); // '{"visible":"public"}'

// 面试题 4:实现一个单例模式(使用 Symbol)
const SINGLETON_KEY = Symbol.for('app.singleton');

class Singleton {
  constructor() {
    if (window[SINGLETON_KEY]) {
      return window[SINGLETON_KEY];
    }
    window[SINGLETON_KEY] = this;
  }
}

const s1 = new Singleton();
const s2 = new Singleton();
console.log(s1 === s2); // true

// 面试题 5:Symbol 在 Redux action type 中的应用
const ActionTypes = {
  ADD_TODO: Symbol('ADD_TODO'),
  REMOVE_TODO: Symbol('REMOVE_TODO'),
  TOGGLE_TODO: Symbol('TOGGLE_TODO')
};

// 确保 action type 不会冲突
function reducer(state = [], action) {
  switch (action.type) {
    case ActionTypes.ADD_TODO:
      return [...state, action.payload];
    case ActionTypes.REMOVE_TODO:
      return state.filter(todo => todo.id !== action.payload);
    default:
      return state;
  }
}