跳到主要内容

Symbol 与迭代器

问题

Symbol 是什么?有什么应用场景?什么是迭代器和迭代协议?

答案

Symbol 是 ES6 引入的第七种原始数据类型,代表独一无二的值。迭代器是一种统一的遍历机制,让不同数据结构可以用相同方式遍历。

Symbol 基础

创建 Symbol

// 创建 Symbol
const sym1 = Symbol();
const sym2 = Symbol();
console.log(sym1 === sym2); // false - 每个 Symbol 都是唯一的

// 带描述的 Symbol
const sym3 = Symbol('description');
console.log(sym3.toString()); // 'Symbol(description)'
console.log(sym3.description); // 'description'

// Symbol 不能用 new
// new Symbol(); // ❌ TypeError

Symbol.for / Symbol.keyFor

// Symbol.for: 全局 Symbol 注册表
const globalSym1 = Symbol.for('app.id');
const globalSym2 = Symbol.for('app.id');
console.log(globalSym1 === globalSym2); // true

// Symbol.keyFor: 获取全局 Symbol 的 key
const key = Symbol.keyFor(globalSym1);
console.log(key); // 'app.id'

// 普通 Symbol 没有 key
const localSym = Symbol('local');
console.log(Symbol.keyFor(localSym)); // undefined

Symbol 应用场景

1. 唯一属性键

// 避免属性名冲突
const id = Symbol('id');

const user = {
name: 'Alice',
[id]: 12345
};

console.log(user[id]); // 12345
console.log(Object.keys(user)); // ['name'] - Symbol 属性不被枚举

// 获取 Symbol 属性
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
console.log(Reflect.ownKeys(user)); // ['name', Symbol(id)]

2. 定义常量

// 使用 Symbol 作为常量值
const STATUS = {
PENDING: Symbol('pending'),
FULFILLED: Symbol('fulfilled'),
REJECTED: Symbol('rejected')
};

function handleStatus(status: symbol): void {
switch (status) {
case STATUS.PENDING:
console.log('等待中');
break;
case STATUS.FULFILLED:
console.log('已完成');
break;
case STATUS.REJECTED:
console.log('已拒绝');
break;
}
}

// 不会与其他值冲突
handleStatus(STATUS.PENDING);

3. 私有属性

// 使用 Symbol 模拟私有属性
const _count = Symbol('count');
const _increment = Symbol('increment');

class Counter {
[_count] = 0;

[_increment](): void {
this[_count]++;
}

increment(): void {
this[_increment]();
}

get count(): number {
return this[_count];
}
}

const counter = new Counter();
counter.increment();
console.log(counter.count); // 1
// console.log(counter[_count]); // 需要 _count 才能访问

内置 Symbol

ES6 定义了一系列内置 Symbol,用于修改对象的默认行为。

Symbol.iterator

// 定义对象的迭代行为
const range = {
start: 1,
end: 5,
[Symbol.iterator](): Iterator<number> {
let current = this.start;
const end = this.end;
return {
next(): IteratorResult<number> {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
}
};
}
};

for (const num of range) {
console.log(num); // 1, 2, 3, 4, 5
}

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

Symbol.toStringTag

// 自定义 Object.prototype.toString 的输出
class MyClass {
get [Symbol.toStringTag](): string {
return 'MyClass';
}
}

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

Symbol.toPrimitive

// 自定义对象转原始值的行为
const obj = {
[Symbol.toPrimitive](hint: string): number | string {
switch (hint) {
case 'number':
return 42;
case 'string':
return 'hello';
default:
return 'default';
}
}
};

console.log(+obj); // 42 (hint: 'number')
console.log(`${obj}`); // 'hello' (hint: 'string')
console.log(obj + ''); // 'default' (hint: 'default')

Symbol.hasInstance

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

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

其他内置 Symbol

// Symbol.isConcatSpreadable
const arr = [1, 2];
const obj = { 0: 3, 1: 4, length: 2, [Symbol.isConcatSpreadable]: true };
console.log(arr.concat(obj)); // [1, 2, 3, 4]

// Symbol.species
class MyArray extends Array {
static get [Symbol.species](): typeof Array {
return Array; // 派生方法返回 Array 而非 MyArray
}
}

// Symbol.match / Symbol.replace / Symbol.search / Symbol.split
// 用于正则表达式相关方法

// Symbol.unscopables
// 用于 with 语句(已不推荐使用)

迭代器(Iterator)

迭代协议

手动实现迭代器

// 迭代器协议
interface IteratorResult<T> {
value: T;
done: boolean;
}

interface Iterator<T> {
next(): IteratorResult<T>;
}

// 可迭代协议
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}

// 实现可迭代对象
function createIterator<T>(items: T[]): Iterable<T> {
return {
[Symbol.iterator](): Iterator<T> {
let index = 0;
return {
next(): IteratorResult<T> {
if (index < items.length) {
return { value: items[index++], done: false };
}
return { value: undefined as T, done: true };
}
};
}
};
}

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

内置可迭代对象

// 数组
for (const item of [1, 2, 3]) {}

// 字符串
for (const char of 'hello') {}

// Map
for (const [key, value] of new Map([['a', 1]])) {}

// Set
for (const item of new Set([1, 2, 3])) {}

// arguments
function foo(): void {
for (const arg of arguments) {}
}

// NodeList
// for (const node of document.querySelectorAll('div')) {}

使用迭代器的场景

const arr = [1, 2, 3];

// for...of
for (const item of arr) {}

// 展开运算符
const copy = [...arr];

// 解构赋值
const [first, second] = arr;

// Array.from
Array.from(arr);

// Promise.all / Promise.race
Promise.all(arr.map(n => Promise.resolve(n)));

// Map / Set 构造函数
new Set(arr);
new Map(arr.map(n => [n, n]));

Generator

Generator 函数是更简洁的迭代器实现方式。

基本用法

// Generator 函数
function* numberGenerator(): Generator<number, string, unknown> {
yield 1;
yield 2;
yield 3;
return 'done';
}

const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 'done', done: true }

// for...of 遍历(不包括 return 值)
for (const num of numberGenerator()) {
console.log(num); // 1, 2, 3
}

yield* 委托

function* gen1(): Generator<number> {
yield 1;
yield 2;
}

function* gen2(): Generator<number> {
yield* gen1(); // 委托给 gen1
yield 3;
}

console.log([...gen2()]); // [1, 2, 3]

// 遍历嵌套数组
function* flatten(arr: any[]): Generator<any> {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item);
} else {
yield item;
}
}
}

console.log([...flatten([1, [2, [3, 4]], 5])]); // [1, 2, 3, 4, 5]

Generator 双向通信

function* calculator(): Generator<number, void, number> {
let result = 0;
while (true) {
const input = yield result;
result += input;
}
}

const calc = calculator();
console.log(calc.next()); // { value: 0, done: false }
console.log(calc.next(5)); // { value: 5, done: false }
console.log(calc.next(10)); // { value: 15, done: false }

使用 Generator 实现可迭代类

class Collection<T> {
private items: T[] = [];

add(item: T): void {
this.items.push(item);
}

*[Symbol.iterator](): Generator<T> {
for (const item of this.items) {
yield item;
}
}

*reversed(): Generator<T> {
for (let i = this.items.length - 1; i >= 0; i--) {
yield this.items[i];
}
}
}

const collection = new Collection<number>();
collection.add(1);
collection.add(2);
collection.add(3);

console.log([...collection]); // [1, 2, 3]
console.log([...collection.reversed()]); // [3, 2, 1]

内置 Symbol 一览

Symbol用途
Symbol.iterator定义默认迭代器
Symbol.asyncIterator定义异步迭代器
Symbol.toStringTag自定义类型标签
Symbol.toPrimitive自定义类型转换
Symbol.hasInstance自定义 instanceof
Symbol.isConcatSpreadable自定义 concat 展开
Symbol.species定义派生对象的构造函数
Symbol.match/replace/search/split正则相关

常见面试问题

Q1: Symbol 的特点是什么?

答案

  1. 唯一性:每个 Symbol 都是独一无二的
  2. 不可枚举:不会出现在 for...in 和 Object.keys 中
  3. 原始类型:typeof 返回 'symbol'
  4. 不能 new:不能用 new 调用
  5. 可作为键:可以作为对象属性键

Q2: for...of 可以遍历哪些对象?

答案

实现了 [Symbol.iterator] 方法的对象都可以用 for...of 遍历:

  • Array
  • String
  • Map
  • Set
  • TypedArray
  • arguments
  • NodeList
  • Generator 对象
  • 自定义可迭代对象

Q3: Iterator 和 Generator 的关系?

答案

  • Iterator 是一种协议,定义了 next() 方法
  • Generator 是更简洁的迭代器实现方式
  • Generator 函数返回的对象同时实现了迭代器协议和可迭代协议
function* gen(): Generator<number> {
yield 1;
yield 2;
}

const g = gen();
// g 既是迭代器(有 next 方法)
console.log(g.next()); // { value: 1, done: false }
// 也是可迭代对象(有 Symbol.iterator)
console.log(g[Symbol.iterator]() === g); // true

Q4: 如何让普通对象可迭代?

答案

const obj = {
a: 1,
b: 2,
c: 3,
*[Symbol.iterator](): Generator<[string, number]> {
for (const key of Object.keys(this)) {
yield [key, this[key as keyof typeof this] as number];
}
}
};

for (const [key, value] of obj) {
console.log(key, value);
}
// a 1
// b 2
// c 3

Q5: Symbol.for 和 Symbol() 的区别?

答案

特性Symbol()Symbol.for()
唯一性每次创建新的全局共享
注册表不注册注册到全局
跨 realm不共享共享
获取 key无法获取Symbol.keyFor
// Symbol() 每次都是新的
Symbol('id') === Symbol('id'); // false

// Symbol.for() 全局共享
Symbol.for('id') === Symbol.for('id'); // true

相关链接