系列文章目录
《JavaScript 基础与进阶笔记》(前期偏基础巩固与常见面试点,后续进入闭包、异步、工程化等进阶主题)
- 第 01 篇:数据类型与类型判断
- 第 02 篇:变量声明与作用域
- 第 03 篇:闭包与高阶函数
- 第 04 篇:函数工厂
- 第 05 篇:this 指向与绑定
- 第 06 篇:原型与原型链
- 第 07 篇:类与继承
- 第 08 篇:JS 执行机制与异步队列
- 第 09 篇:数组常用方法
- 第 10 篇:字符串算法
- 第 11 篇:常见手写题合集(上)
- 第 12 篇:常见手写题合集(下)
- 第 13 篇:Promise 与 async/await
- 第 14 篇:数据结构基础
- 第 15 篇:垃圾回收与内存
- 第 16 篇:DOM 基础全面解析
- 第 17 篇:DOM 性能与渲染
- 第 18 篇:DOM 交互补充
- 第 19 篇:DOM 实战案例
- 第 20 篇:CSS 布局与可视化高频
- 第 21 篇:移动端与 viewport
- 第 22 篇:BOM 核心对象
- 第 23 篇:前端路由原理
- 第 24 篇:浏览器存储对比
- 第 25 篇:网络与跨域
- 第 26 篇:网络请求与实时通道
- 第 27 篇:Service Worker、PWA 与 Web Worker
- 第 28 篇:浏览器高级 API
- 第 29 篇:图片懒加载
- 第 30 篇:ES6+ 模块
- 第 31 篇:Symbol 与 Iterator / Generator(本文)
文章目录
- 系列文章目录
- 前言
- 一、Symbol:不只是"不会重复的键"
-
- [1.1 诞生的背景](#1.1 诞生的背景)
- [1.2 Symbol 的本质](#1.2 Symbol 的本质)
- [1.3 Symbol 作为属性键](#1.3 Symbol 作为属性键)
- [1.4 Symbol.for 与全局注册表](#1.4 Symbol.for 与全局注册表)
- [二、Well-Known Symbols:语言的"后门"](#二、Well-Known Symbols:语言的"后门")
-
-
- [自定义 `instanceof`](#自定义
instanceof) - 自定义类型标签
- [自定义 `instanceof`](#自定义
-
- 三、迭代协议:从"怎么遍历"到"什么是可遍历的"
-
- [3.1 问题背景](#3.1 问题背景)
- [3.2 迭代协议的核心思想](#3.2 迭代协议的核心思想)
- [3.3 原生可迭代对象](#3.3 原生可迭代对象)
- [3.4 `for...of` 的解构与展开](#3.4
for...of的解构与展开)
- 四、Generator:迭代器的"语法糖"
-
- [4.1 基本语法](#4.1 基本语法)
- [4.2 Generator 与迭代协议的关系](#4.2 Generator 与迭代协议的关系)
- [4.3 `yield*` 委托](#4.3
yield*委托) - [4.4 双向通信:`next(value)`](#4.4 双向通信:
next(value)) - [4.5 `return()` 与 `throw()`](#4.5
return()与throw())
- [五、异步迭代:`for await...of`](#五、异步迭代:
for await...of) - [六、Generator 的实际应用模式](#六、Generator 的实际应用模式)
-
- [6.1 状态机](#6.1 状态机)
- [6.2 惰性求值](#6.2 惰性求值)
- [6.3 替代回调地狱](#6.3 替代回调地狱)
- [6.4 中间件模式(Koa 风格)](#6.4 中间件模式(Koa 风格))
- 七、设计层面的思考
-
- [7.1 为什么需要协议而不是语法?](#7.1 为什么需要协议而不是语法?)
- [7.2 Generator 的协程本质](#7.2 Generator 的协程本质)
- [7.3 为什么普通对象默认不可迭代?](#7.3 为什么普通对象默认不可迭代?)
- 八、易混淆点
- 九、思考与练习
- 总结
前言
第 30 篇讲了模块化是工程化的基石;本篇进入 语言机制层面 的两个重要概念:Symbol 与 迭代协议。
Symbol 不是简单的"第七种基本类型"------它是 JavaScript 在元编程 方向上的关键一步。通过 Symbol.iterator,语言把"什么对象可以被 for...of 遍历"这个问题,从语法层面 下放到了协议层面。而 Generator 函数,则是实现这个协议最自然的工具。
本篇不打算罗列 API,而是从设计动机出发,讲清楚:为什么需要 Symbol?迭代协议解决了什么问题?Generator 和普通函数的本质区别是什么?
一、Symbol:不只是"不会重复的键"
1.1 诞生的背景
ES5 时代,对象属性名只能是字符串。这意味着:
javascript
const obj = {};
obj["name"] = "Alice";
obj["name"] = "Bob"; // 直接覆盖,没有任何警告
在大型项目中,不同模块往同一个对象上挂属性(比如给 DOM 元素加元数据、给第三方库的实例加标记),命名冲突 是真实存在的痛点。你没法保证你的 id 属性不会和别人的 id 冲突。
Symbol 就是为了解决这个问题:每个 Symbol 值都是唯一的,即使描述相同。
javascript
const s1 = Symbol("key");
const s2 = Symbol("key");
s1 === s2; // false
1.2 Symbol 的本质
Symbol 是 ES6 新增的第七种基本类型 (primitive type),和 number、string 同级。
关键特性:
- 不可
new:Symbol()是函数调用,不是构造器。new Symbol()会报错。 - 隐式转换会报错 :
Symbol("a") + "b"抛 TypeError,因为 Symbol 不能转为数字或字符串参与运算。 - 可显式转字符串 :
String(Symbol("a"))→"Symbol(a)";Symbol("a").toString()→"Symbol(a)"。 - 可转布尔 :
Boolean(Symbol())→true,Symbol 不是 falsy 值。
javascript
const s = Symbol("id");
// 不能参与运算
// s + 1; // TypeError
// 可以显式转换
console.log(String(s)); // "Symbol(id)"
console.log(!!s); // true
1.3 Symbol 作为属性键
Symbol 可以作为对象属性名,这是它最核心的用途:
javascript
const LOG_LEVEL = Symbol("log_level");
const config = {
[LOG_LEVEL]: "debug",
name: "my-app"
};
console.log(config[LOG_LEVEL]); // "debug"
遍历行为的差异------这是面试常考点:
javascript
const sym = Symbol("hidden");
const obj = { [sym]: "secret", visible: "hello" };
// 以下三个方法【不】会枚举 Symbol 属性
Object.keys(obj); // ["visible"]
Object.getOwnPropertyNames(obj); // ["visible"]
for (const key in obj) {} // 只遍历 "visible"
// 以下方法【会】包含 Symbol 属性
Object.getOwnPropertySymbols(obj); // [Symbol(hidden)]
Reflect.ownKeys(obj); // ["visible", Symbol(hidden)]
为什么这样设计? 因为 Symbol 属性的一个重要用途就是"半私有"------你不想让它出现在普通的遍历中,避免被意外覆盖或序列化(JSON.stringify 也会忽略 Symbol 键)。
1.4 Symbol.for 与全局注册表
Symbol() 每次都创建全新值。但有些场景需要跨模块共享同一个 Symbol(比如多个文件都想用同一个 key 来标记某个协议)。
javascript
// a.js
const key = Symbol.for("shared_key");
// b.js
const key = Symbol.for("shared_key");
// a.js 的 key === b.js 的 key → true
Symbol.for() 会在一个全局注册表 中查找,有则返回已有的,没有则创建并注册。这个注册表本质是一个 [[GlobalSymbolRegistry]] 内部槽,以字符串描述为 key。
对比:
| 方式 | 作用域 | 适用场景 |
|---|---|---|
Symbol("desc") |
当前调用 | 模块内部私有键 |
Symbol.for("key") |
全局注册表 | 跨模块共享、协议约定 |
javascript
// 判断是否是全局注册的 Symbol
Symbol.keyFor(Symbol.for("test")); // "test"
Symbol.keyFor(Symbol("test")); // undefined
二、Well-Known Symbols:语言的"后门"
JavaScript 预定义了一批 Well-Known Symbol ,它们是挂在 Symbol 构造器上的特殊属性,用来定制对象的底层行为。你可以把它们理解为语言给开发者留的"钩子"。
最常用的几个:
| Well-Known Symbol | 控制什么 |
|---|---|
Symbol.iterator |
对象是否可迭代、如何迭代 |
Symbol.asyncIterator |
异步迭代协议 |
Symbol.toPrimitive |
类型转换时的行为 |
Symbol.hasInstance |
instanceof 的判断逻辑 |
Symbol.toStringTag |
Object.prototype.toString() 的标签 |
Symbol.species |
派生对象的构造器 |
自定义 instanceof
javascript
class EvenNumber {
static [Symbol.hasInstance](num) {
return typeof num === "number" && num % 2 === 0;
}
}
4 instanceof EvenNumber; // true
3 instanceof EvenNumber; // false
自定义类型标签
javascript
class MyCollection {
get [Symbol.toStringTag]() {
return "MyCollection";
}
}
Object.prototype.toString.call(new MyCollection()); // "[object MyCollection]"
这些机制体现了 JavaScript 的元编程能力:语言行为本身可以通过 Symbol 来定制,而不是只能被动接受。
三、迭代协议:从"怎么遍历"到"什么是可遍历的"
3.1 问题背景
ES5 遍历对象的方式很混乱:
- 数组 :
for循环、forEach、for...in(会遍历原型链和非数字键) - 字符串 :
for循环按索引 - 对象 :
for...in+hasOwnProperty过滤 - DOM NodeList :
for循环,但不是数组
每种数据结构的遍历方式都不一样,无法写出统一的遍历代码。
3.2 迭代协议的核心思想
ES6 引入了迭代协议(Iteration Protocol),核心就一句话:
任何实现了
Symbol.iterator方法的对象,都是可迭代的(iterable)。
这个方法必须返回一个迭代器对象 (iterator),迭代器对象必须有 next() 方法,next() 返回 { value, done }。
可迭代对象 (iterable)
└── [Symbol.iterator]() → 迭代器 (iterator)
└── next() → { value, done }
javascript
// 手动实现一个可迭代对象
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { done: true };
}
};
}
};
for (const n of range) {
console.log(n); // 1, 2, 3, 4, 5
}
关键洞察 :for...of 循环的本质就是调用 [Symbol.iterator]() 获取迭代器,然后反复调用 next() 直到 done 为 true。它不是语法糖,而是协议驱动的。
3.3 原生可迭代对象
JavaScript 中内置的可迭代对象包括:
Array、TypedArrayStringMap、SetargumentsNodeListTypedArrayGenerator对象(后面会讲)
javascript
const arr = [10, 20, 30];
const iter = arr[Symbol.iterator]();
iter.next(); // { value: 10, done: false }
iter.next(); // { value: 20, done: false }
iter.next(); // { value: 30, done: false }
iter.next(); // { value: undefined, done: true }
注意:普通对象默认不是可迭代的!
javascript
const obj = { a: 1, b: 2 };
// for (const v of obj) {} // TypeError: obj is not iterable
这是有意为之的。对象的键有顺序争议(数字键会自动排序),ES6 委员会不想武断决定遍历顺序。如果你需要遍历对象,用 Object.keys/values/entries 或 Object.entries。
3.4 for...of 的解构与展开
由于迭代协议的存在,所有可迭代对象都可以:
javascript
// 解构
const [a, b] = new Set([1, 2, 3]); // a=1, b=2
// 展开
const arr = [...new Set([1, 2, 2, 3])]; // [1, 2, 3]
// Array.from
const chars = Array.from("hello"); // ["h", "e", "l", "l", "o"]
// Promise.all、Map 构造器等都接受可迭代对象
const map = new Map([["a", 1], ["b", 2]]);
这就是协议的力量:一旦实现了 Symbol.iterator,就能无缝接入整个语言生态。
四、Generator:迭代器的"语法糖"
手动写迭代器对象很繁琐------你需要维护闭包状态、返回 { value, done }。Generator 函数就是让这件事变得简单。
4.1 基本语法
javascript
function* countTo3() {
yield 1;
yield 2;
yield 3;
}
const gen = countTo3();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.next(); // { value: 3, done: false }
gen.next(); // { value: undefined, done: true }
function* 声明一个 Generator 函数。调用它不会执行函数体 ,而是返回一个Generator 对象 (同时也是迭代器)。每次调用 next(),函数体执行到下一个 yield 语句暂停。
本质理解 :Generator 是一种可以暂停和恢复的函数 。yield 就是暂停点,next() 就是恢复执行。
4.2 Generator 与迭代协议的关系
Generator 对象自带 Symbol.iterator,且返回自身:
javascript
function* gen() { yield 1; }
const g = gen();
g[Symbol.iterator]() === g; // true
这意味着 Generator 对象天然就是可迭代的迭代器 。所以用 Generator 实现 range 会简洁很多:
javascript
function* range(from, to) {
for (let i = from; i <= to; i++) {
yield i;
}
}
for (const n of range(1, 5)) {
console.log(n); // 1, 2, 3, 4, 5
}
4.3 yield* 委托
yield* 可以把迭代委托给另一个可迭代对象:
javascript
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
[...concat([1, 2], [3, 4])]; // [1, 2, 3, 4]
递归 Generator + yield* 可以优雅地处理树形结构:
javascript
function* flatten(arr) {
for (const item of arr) {
if (Array.isArray(item)) {
yield* flatten(item); // 递归委托
} else {
yield item;
}
}
}
[...flatten([1, [2, [3, 4]], 5])]; // [1, 2, 3, 4, 5]
4.4 双向通信:next(value)
Generator 不只是产出值,还可以接收值 。next() 的参数会成为上一个 yield 表达式的返回值:
javascript
function* accumulator() {
let sum = 0;
while (true) {
const value = yield sum; // yield 产出 sum,同时接收 next 传入的值
sum += value;
}
}
const acc = accumulator();
acc.next(); // { value: 0, done: false } --- 初始 yield 产出 0
acc.next(10); // { value: 10, done: false } --- yield 返回 10,sum=10,产出 10
acc.next(20); // { value: 30, done: false } --- yield 返回 20,sum=30,产出 30
acc.next(5); // { value: 35, done: false } --- yield 返回 5,sum=35,产出 35
这个特性是 async/await 实现的底层基础------Babel 把 async/await 转译成 Generator + Promise 就是利用了双向通信。
4.5 return() 与 throw()
迭代器协议还支持两个额外的方法:
javascript
function* gen() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("cleanup");
}
}
const g = gen();
g.next(); // { value: 1, done: false }
g.return("bye"); // "cleanup" 被打印,{ value: "bye", done: true }
return(value) 会强制结束 Generator,但会先执行 finally 块。这和 for...of 中 break 的行为一致------for...of 遇到 break 时会自动调用迭代器的 return()。
throw(error) 则是在 Generator 暂停的地方抛出一个异常:
javascript
const g = gen();
g.next(); // { value: 1, done: false }
g.throw(new Error("oops")); // 在 yield 1 处抛出错误,进入 catch(如果有)
五、异步迭代:for await...of
5.1 异步迭代协议
有些数据源是异步到达 的------比如逐行读取大文件、WebSocket 消息流、分页 API。这时候需要异步迭代器:
javascript
// 异步迭代器:next() 返回 Promise
const asyncIterator = {
i: 0,
next() {
if (this.i < 3) {
return Promise.resolve({ value: this.i++, done: false });
}
return Promise.resolve({ done: true });
},
[Symbol.asyncIterator]() {
return this;
}
};
异步可迭代对象需要实现 Symbol.asyncIterator 方法(注意是 asyncIterator,不是 iterator)。
5.2 for await...of 语法
javascript
async function consume() {
for await (const value of asyncIterator) {
console.log(value); // 0, 1, 2
}
}
for await...of 会等待每个 next() 返回的 Promise resolve 后再继续。
5.3 异步 Generator
和普通 Generator 类似,异步 Generator 可以在 yield 前加 await:
javascript
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url);
const data = await res.json();
yield data; // yield 的值会被包装成 Promise
}
}
async function main() {
const urls = ["/api/page1", "/api/page2", "/api/page3"];
for await (const data of fetchPages(urls)) {
console.log(data);
}
}
异步 Generator 的返回值 同时实现了 Symbol.asyncIterator,所以可以用 for await...of 消费。
5.4 实际应用场景
场景一:节流数据流
javascript
async function* throttle(asyncIter, delay) {
for await (const value of asyncIter) {
yield value;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
场景二:取消迭代
javascript
async function* cancellable(asyncIter, signal) {
for await (const value of asyncIter) {
if (signal.aborted) return;
yield value;
}
}
场景三:批处理
javascript
async function* batch(asyncIter, size) {
let batch = [];
for await (const value of asyncIter) {
batch.push(value);
if (batch.length >= size) {
yield batch;
batch = [];
}
}
if (batch.length > 0) yield batch;
}
六、Generator 的实际应用模式
Generator 不只是用来创建迭代器,在实际工程中有很多巧妙的用法。
6.1 状态机
Generator 天然适合表达状态机------每个 yield 就是一个状态:
javascript
function* trafficLight() {
while (true) {
yield "red";
yield "green";
yield "yellow";
}
}
const light = trafficLight();
light.next().value; // "red"
light.next().value; // "green"
light.next().value; // "yellow"
light.next().value; // "red"(循环)
6.2 惰性求值
Generator 只在需要时才计算下一个值,适合处理大数据集:
javascript
function* fibonacci() {
let [a, b] = [0, 1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
// 只取前 10 个,不会无限计算
const first10 = [...fibonacci()].slice(0, 10);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
6.3 替代回调地狱
在 async/await 普及之前,Generator + Promise 是解决回调地狱的方案(co 库的原理):
javascript
// 这是 async/await 的底层原理示意
function runGenerator(genFn) {
const gen = genFn();
function step(value) {
const result = gen.next(value);
if (result.done) return result.value;
return Promise.resolve(result.value).then(
val => step(val),
err => gen.throw(err)
);
}
return step();
}
// 等价于 async/await
runGenerator(function* () {
const user = yield fetchUser(1);
const posts = yield fetchPosts(user.id);
console.log(posts);
});
6.4 中间件模式(Koa 风格)
Koa 1.x 使用 Generator 实现洋葱模型中间件:
javascript
function* middleware1(next) {
console.log("1 before");
yield next;
console.log("1 after");
}
function* middleware2(next) {
console.log("2 before");
yield next;
console.log("2 after");
}
七、设计层面的思考
7.1 为什么需要协议而不是语法?
如果 for...of 只能用于数组,那它就是语法糖。但迭代协议让它可以用于任何对象 ------这就是鸭子类型(duck typing)的体现:不看你是什么类型,看你有什么行为。
这种设计的影响深远:
Map和Set不需要是数组,但可以被for...of遍历- 你可以让自定义类支持解构、展开
- 第三方库可以实现自定义可迭代对象,无缝接入语言生态
7.2 Generator 的协程本质
Generator 本质上是一种协程 (coroutine)------比线程更轻量的并发单元。yield 不是简单地"产出值",而是让出执行权。
这和 async/await 的关系:
async/await是 Generator + Promise 的语法糖- Babel 转译
async/await时,底层就是用 Generator 实现的 await≈yield,自动执行器 ≈co
javascript
// async/await
async function fetchUser(id) {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// 等价的 Generator 版本
function* fetchUser(id) {
const res = yield fetch(`/api/users/${id}`);
return yield res.json();
}
7.3 为什么普通对象默认不可迭代?
这是一个有意的设计决策:
- 对象的键顺序有争议:数字键会被自动排序到前面,字符串键按插入顺序。这可能导致意外行为。
- 枚举语义不同 :
for...in会遍历原型链,而for...of不会。直接让对象支持for...of可能让人误以为不会遍历原型属性。 - 显式优于隐式 :如果你需要遍历对象,明确使用
Object.keys()、Object.entries()等,意图更清晰。
八、易混淆点
Symbol()vsSymbol.for():前者每次创建新值,后者全局注册表共享。Object.keys()不包含 Symbol 键 :需要Reflect.ownKeys()或Object.getOwnPropertySymbols()。for...invsfor...of:for...in遍历键名 (字符串,含原型链);for...of遍历可迭代对象的值。- Generator 调用不执行函数体 :
function* f() { console.log("hi"); }调用f()不会打印,需要f().next()才执行。 yield是表达式,不是语句 :const x = yield value是合法的,yield表达式的值由下一次next(arg)的参数决定。for await...of用于异步迭代 :同步可迭代对象用for...of,异步的用for await...of,不要混用。yield*委托 vs 直接 yield :yield*会逐个产出被委托对象的所有值;直接yield只产出对象本身。- Generator 的
return()会触发finally:和普通函数的return类似,但 Generator 是在暂停状态被终止的。
九、思考与练习
1. Symbol("foo") === Symbol("foo") 的结果是什么?为什么?
解析:false 。Symbol() 每次创建的都是唯一值,即使描述相同。如果需要共享,用 Symbol.for("foo")。
2. 以下代码输出什么?
javascript
const obj = { [Symbol()]: 1 };
const clone = { ...obj };
console.log(Object.getOwnPropertySymbols(clone).length);
解析:1 。... 展开会复制 Symbol 键属性(Object.assign 也会)。
3. 如何让一个普通对象可以被 for...of 遍历?
解析:给对象添加 [Symbol.iterator] 方法,返回一个有 next() 的迭代器对象。最简方式是用 Generator:
javascript
const obj = {
a: 1, b: 2, c: 3,
*[Symbol.iterator]() {
for (const key of Object.keys(this)) {
yield this[key];
}
}
};
for (const v of obj) console.log(v); // 1, 2, 3
4. Generator 函数中的 yield 和 return 有什么区别?
解析:
yield暂停执行,产出值,下次next()可以继续return终止 Generator,done变为trueyield可以多次使用,return只能一次(之后的next()都返回{ done: true })
5. for await...of 和 Promise.all 的使用场景有什么区别?
解析:
for await...of是串行 处理:等上一个 Promise 完成再处理下一个,适合流式数据Promise.all是并行 处理:同时发起所有请求,适合批量独立操作
6. 为什么 Generator 适合实现惰性求值?
解析:Generator 按需执行 ------只有调用 next() 时才计算下一个值。这意味着可以处理无限序列(如斐波那契数列),只取需要的部分,不会内存溢出。
总结
- Symbol 解决了属性名冲突问题,
Symbol.iterator是迭代协议的入口。 - 迭代协议 统一了数据遍历方式:实现了
[Symbol.iterator]的对象都可以用for...of、解构、展开。 - Generator 是实现迭代器的最简方式:
function*+yield,自带[Symbol.iterator]。 - 异步迭代 通过
Symbol.asyncIterator+for await...of处理异步数据流。 - Generator 的本质是协程 ,
yield是暂停点,next()是恢复点------这也是async/await的底层原理。
下一篇讲 Proxy 与 Reflect :13 种拦截 trap、Reflect 与 receiver 的关系、以及 Vue 3 为什么选择 Proxy(系列第 32 篇,大纲 §32)。