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]
面试要点
- 唯一性:每个 Symbol 都是唯一的,即使描述相同
- 原始类型:Symbol 是原始数据类型,不是对象
- 不可枚举:Symbol 属性不会出现在 for...in、Object.keys() 中
- 应用场景:私有属性、常量定义、元数据存储、消除魔术字符串
- 内置 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;
}
}