Symbol 与迭代协议
Symbol
是一种原始数据类型,常用于创建独一无二的标识;Symbol.iterator
是迭代协议的关键,让对象可被 for...of
、展开等消费。
Symbol 基础
javascript
const KEY = Symbol("unique");
const obj = { [KEY]: 42 };
console.log(obj[KEY]); // 42(避免与普通键冲突)
1
2
3
2
3
可迭代对象与 Symbol.iterator
javascript
const range = {
start: 1,
end: 3,
[Symbol.iterator]() {
let current = this.start;
const end = this.end;
return {
next() {
if (current <= end) {
return { value: current++, done: false };
}
return { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
要点:
- 实现
Symbol.iterator
即可迭代; - 数组、字符串、Map、Set 等内置类型已内置可迭代性;
- 生成器(
function*
)可更优雅地实现迭代逻辑。
Symbol 进阶:全局注册表与描述
javascript
const s1 = Symbol("desc");
const s2 = Symbol("desc");
console.log(s1 === s2); // false(每次都是唯一的)
// 全局注册表:可通过 key 复用相同 symbol
const g1 = Symbol.for("APP_TOKEN");
const g2 = Symbol.for("APP_TOKEN");
console.log(g1 === g2); // true
console.log(Symbol.keyFor(g1)); // 'APP_TOKEN'
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
常见内置(Well-known)Symbols
Symbol.iterator
:同步迭代协议入口;Symbol.asyncIterator
:异步迭代协议入口(for await...of
);Symbol.toStringTag
:自定义对象的[object X]
标签;Symbol.toPrimitive
:自定义对象到原始值的转换。
javascript
const obj = {
[Symbol.toStringTag]: "Vector2",
x: 1,
y: 2,
[Symbol.toPrimitive](hint) {
return hint === "number"
? Math.hypot(this.x, this.y)
: `${this.x},${this.y}`;
},
};
console.log(Object.prototype.toString.call(obj)); // [object Vector2]
console.log(+obj); // 数字上下文 -> 长度
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
迭代协议细节
- 可迭代对象:拥有
obj[Symbol.iterator]()
,返回一个迭代器; - 迭代器:拥有
next()
方法,返回{ value, done }
; - for...of 会自动调用迭代器直至
done: true
; - 生成器天然实现迭代器接口。
javascript
const iter = [10, 20][Symbol.iterator]();
console.log(iter.next()); // { value:10, done:false }
console.log(iter.next()); // { value:20, done:false }
console.log(iter.next()); // { value:undefined, done:true }
1
2
3
4
2
3
4
构建自定义可迭代
javascript
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
let cur = this.start,
end = this.end;
return {
next() {
if (cur <= end) return { value: cur++, done: false };
return { value: undefined, done: true };
},
};
}
}
console.log([...new Range(3, 6)]); // [3,4,5,6]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
异步可迭代:实现 Symbol.asyncIterator
并返回带 next()
的异步迭代器,可用于 for await...of
。
for...of 与 for...in 的区别
for...of
遍历“值”,基于迭代协议(数组、字符串、Set、Map、可迭代对象);for...in
遍历“可枚举属性名”(字符串键),会枚举原型链上的可枚举属性,不适合数组。
javascript
for (const v of ["a", "b"]) console.log(v); // a b
for (const k in ["a", "b"]) console.log(k); // 0 1(键名)
1
2
2
常见陷阱
- 忘记实现
[Symbol.iterator]
却使用for...of
/展开; - 在自定义迭代器中返回的对象未遵守
{ value, done }
协议; - 将
for...in
用于数组导致意外属性遍历; - 滥用
Symbol.for
导致跨模块共享键名冲突(需命名规范)。
最佳实践
- 需要惰性序列/自定义遍历顺序时,实现迭代协议或使用生成器;
- 迭代器中保持纯粹、无副作用的
next()
,便于调试与复用; - 使用
for...of
遍历数组/集合结构,避免for...in
; - 约定
Symbol.for
命名空间前缀(如app.module.key
)。
练习
- 为一个二维网格实现迭代:按行或按列迭代单元格;
- 将自定义可迭代改写为生成器版本,比较可读性;
- 实现异步可迭代,从多个分页接口顺序拉取数据并
for await...of
消费。