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 的特点是什么?
答案:
- 唯一性:每个 Symbol 都是独一无二的
- 不可枚举:不会出现在 for...in 和 Object.keys 中
- 原始类型:typeof 返回 'symbol'
- 不能 new:不能用 new 调用
- 可作为键:可以作为对象属性键
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