第 33 章

迭代器与可迭代协议:for...of 的完整内部机制

第33章:迭代器协议与可迭代协议——规范统一接口的设计哲学

for...of 能遍历数组、字符串、Map、Set、生成器、DOM NodeList,靠的不是特殊处理,而是一个统一的鸭子类型协议——任何对象实现这个协议就能被遍历。

本章核心问题:ECMAScript 迭代器协议如何在规范层面统一了数据消费和数据生产,for...of 的规范执行路径是什么,以及如何实现一个完整的自定义迭代器(包括清理资源的 return() 方法)?

读完本章你将理解


Level 1 · 你需要知道的(1-3年经验)

ES2015 引入迭代器协议之前,遍历不同数据结构需要不同的 API:数组用索引,Map 用 forEach,自定义集合没有统一方式。迭代器协议解决了这个问题——任何对象,只要实现了规定的接口,就能被所有消费迭代器的语法(for...of、展开、解构、Array.from)统一处理。

两层协议

可迭代协议(Iterable Protocol):对象必须有一个 [Symbol.iterator] 方法,该方法返回一个迭代器对象。

迭代器协议(Iterator Protocol):迭代器对象必须有一个 next() 方法,该方法返回形如 { value: any, done: boolean } 的对象(IteratorResult)。

// 验证一个对象是否可迭代
function isIterable(obj) {
  return obj != null && typeof obj[Symbol.iterator] === 'function'
}

isIterable([1, 2, 3])     // true
isIterable('hello')        // true
isIterable(new Map())      // true
isIterable(new Set())      // true
isIterable({ a: 1 })       // false(普通对象不可迭代)
isIterable(42)             // false
isIterable(null)           // false

手动使用迭代器

const arr = [10, 20, 30]
const iter = arr[Symbol.iterator]()  // 获取迭代器

console.log(iter.next())  // { value: 10, done: false }
console.log(iter.next())  // { value: 20, done: false }
console.log(iter.next())  // { value: 30, done: false }
console.log(iter.next())  // { value: undefined, done: true }
console.log(iter.next())  // { value: undefined, done: true }(继续调用不报错)

for...of 的工作方式

for...of 是上述手动步骤的语法糖:

// 使用 for...of
for (const x of [10, 20, 30]) {
  console.log(x)
}
// 输出:10, 20, 30

// 等价的手动实现
const _iterable = [10, 20, 30]
const _iter = _iterable[Symbol.iterator]()
let _result
while (!(_result = _iter.next()).done) {
  const x = _result.value
  console.log(x)
}

常见的可迭代对象

类型 迭代结果 示例
Array 每个元素 for (const x of [1,2,3]) → 1, 2, 3
String 每个 Unicode 码点(不是字节) for (const c of '😀')'😀'(一次)
Map [key, value] for (const [k,v] of map)
Set 每个值 for (const v of set)
arguments 每个参数值 for (const a of arguments)
NodeList 每个 DOM 节点 for (const el of document.querySelectorAll('div'))
generator 每个 yield 的值 for (const v of gen())

String 的特殊行为

// String 迭代按 Unicode 码点,而不是 UTF-16 代码单元
const emoji = '😀🎉'
console.log(emoji.length)      // 4(两个 emoji 各占 2 个 UTF-16 代码单元)

const chars = [...emoji]
console.log(chars)             // ['😀', '🎉'](2个元素,按码点迭代)
console.log(chars.length)      // 2

// 传统 for 循环会拆开代理对:
for (let i = 0; i < emoji.length; i++) {
  console.log(emoji[i])  // 输出4次,包括乱码的代理字符
}

Level 2 · 它是怎么运行的(3-5年经验)

生成器函数作为迭代器工厂

生成器函数(Generator Function)是实现可迭代对象最简洁的方式。生成器函数调用时返回一个同时实现了可迭代协议和迭代器协议的对象(称为 Generator 对象):

function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i
  }
}

// range() 返回的对象同时是迭代器和可迭代对象
const r = range(0, 10, 2)
console.log(typeof r[Symbol.iterator])  // 'function'
console.log(typeof r.next)              // 'function'
console.log(r[Symbol.iterator]() === r) // true!生成器对象的 @@iterator 返回自身

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

// 可以多次使用工厂(每次调用 range() 都创建新迭代器)
const nums1 = [...range(1, 4)]   // [1, 2, 3]
const nums2 = [...range(1, 4)]   // [1, 2, 3](新的迭代器)

无限序列与惰性求值

迭代器协议的核心优势之一是惰性求值——只在需要时才生成下一个值:

// 无限 Fibonacci 序列
function* fibonacci() {
  let [a, b] = [0, 1]
  while (true) {
    yield a
    ;[a, b] = [b, a + b]
  }
}

// 取前10个:只计算了10次,而不是生成整个序列
const fib10 = Array.from({ length: 10 }, () => fibonacci().next())
// 上面的写法每次都创建新迭代器,正确写法:
const fibIter = fibonacci()
const first10 = Array.from({ length: 10 }, () => fibIter.next().value)
console.log(first10)  // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

// 链式惰性操作(不产生中间数组)
function* take(n, iterable) {
  let count = 0
  for (const item of iterable) {
    if (count >= n) return
    yield item
    count++
  }
}

function* filter(predicate, iterable) {
  for (const item of iterable) {
    if (predicate(item)) yield item
  }
}

function* map2(fn, iterable) {
  for (const item of iterable) {
    yield fn(item)
  }
}

// 从无限序列中取满足条件的前5个偶数 Fibonacci 数
const result = [...take(5, filter(n => n % 2 === 0, fibonacci()))]
console.log(result)  // [0, 2, 8, 34, 144]
// 这个操作在内存中只保留当前值,没有任何中间数组

迭代器的三个方法

完整的迭代器协议包含三个方法:

// 完整迭代器接口
const iterator = {
  // 必须的
  next(value) {
    // value 是传入 next() 的值(用于与生成器通信)
    return { value: any, done: boolean }
  },

  // 可选的:提前终止迭代时调用
  return(value) {
    // 清理资源(关闭文件、释放数据库连接等)
    return { value: value, done: true }
  },

  // 可选的:向迭代器抛出错误
  throw(error) {
    // 处理或重新抛出错误
    return { value: any, done: boolean }
  }
}

return() 在以下情况被调用:

// 完整的自定义迭代器(包含资源清理)
function createDBCursor(query) {
  let connection = openConnection()
  let cursor = connection.execute(query)
  let closed = false

  return {
    [Symbol.iterator]() { return this },

    next() {
      if (closed || !cursor.hasNext()) {
        closed = true
        connection.close()
        return { value: undefined, done: true }
      }
      return { value: cursor.next(), done: false }
    },

    return(value) {
      if (!closed) {
        closed = true
        connection.close()  // 提前终止时清理连接
        console.log('Connection closed early')
      }
      return { value, done: true }
    }
  }
}

// 使用 break 提前终止,return() 被自动调用
for (const row of createDBCursor('SELECT * FROM users')) {
  if (row.id === targetId) {
    break  // ← 触发 return(),连接被正确关闭
  }
}

可迭代对象 vs 迭代器

一个重要的区分:

// 可迭代对象:每次调用 [Symbol.iterator]() 返回新迭代器
// 迭代器本身:通常只能遍历一次

const arr = [1, 2, 3]  // 可迭代对象

// 可以多次遍历
for (const x of arr) {}  // 第一次
for (const x of arr) {}  // 第二次(全新的迭代器)

// 但直接持有迭代器,就只能遍历一次:
const iter = arr[Symbol.iterator]()  // 迭代器
for (const x of iter) { console.log(x) }  // 1, 2, 3
for (const x of iter) { console.log(x) }  // 没有输出!迭代器已耗尽

// 生成器对象是迭代器(不是可迭代工厂):
function* gen() { yield 1; yield 2 }
const g = gen()  // 迭代器
for (const x of g) {}  // 1, 2
for (const x of g) {}  // 没有输出!g 已耗尽
// 要再次遍历,需要重新调用 gen()

异步迭代协议

ES2018 引入了异步迭代协议,用于处理异步数据流:

// 异步可迭代协议:[Symbol.asyncIterator]() 返回异步迭代器
// 异步迭代器协议:next() 返回 Promise<{value, done}>

// 读取数据流的异步迭代器
async function* readLines(stream) {
  const reader = stream.getReader()
  const decoder = new TextDecoder()

  try {
    while (true) {
      const { value, done } = await reader.read()
      if (done) break
      yield decoder.decode(value)
    }
  } finally {
    reader.releaseLock()  // 清理资源,不论正常还是提前终止
  }
}

// 使用 for await...of
async function processStream(stream) {
  for await (const line of readLines(stream)) {
    console.log(line)
  }
}
// 异步生成器实现分页加载
async function* fetchAllPages(url) {
  let nextUrl = url
  while (nextUrl) {
    const response = await fetch(nextUrl)
    const data = await response.json()
    yield* data.items  // yield* 展开可迭代对象
    nextUrl = data.nextPage
  }
}

for await (const item of fetchAllPages('/api/items')) {
  console.log(item)
}

Level 3 · 规范怎么定义的(资深开发者)

GetIterator 规范算法

for...of 的第一步是 GetIterator,规范定义(§7.4.1):

7.4.1 GetIterator ( obj, kind [ , method ] )

  1. If method is not present, then a. If kind is async, then i. Set method to ? GetMethod(obj, @@asyncIterator). ii. If method is undefined, then 1. Let syncMethod be ? GetMethod(obj, @@iterator). 2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod). 3. Return CreateAsyncFromSyncIterator(syncIteratorRecord). b. Otherwise, set method to ? GetMethod(obj, @@iterator).
  2. Let iterator be ? Call(method, obj).
  3. If iterator is not an Object, throw a TypeError exception.
  4. Let nextMethod be ? GetMethod(iterator, "next").
  5. Let iteratorRecord be the Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }.
  6. Return iteratorRecord.

中文解读

步骤1b:调用 obj[Symbol.iterator],这就是为什么不可迭代对象在 for...of 中会抛出 TypeError: obj is not iterable——GetMethod(obj, @@iterator) 返回 undefined,然后 Call(undefined, obj) 抛出 TypeError。

步骤3:iterator 必须是对象!如果 [Symbol.iterator]() 返回了原始值(如 42),会立即抛出 TypeError。这要求自定义迭代器的 [Symbol.iterator]() 方法必须返回一个对象。

步骤4:nextMethodGetIterator 时被缓存,而不是每次 IteratorStep 时动态查找。这意味着迭代开始后更改 iterator.next 不会影响正在进行的迭代。

步骤1a:异步迭代时,如果对象没有 [Symbol.asyncIterator],引擎会自动回退到 [Symbol.iterator] 并用 CreateAsyncFromSyncIterator 包装,使同步迭代器可以在 for await...of 中使用。

IteratorStep 规范算法

7.4.5 IteratorStep ( iteratorRecord )

  1. Let result be ? IteratorNext(iteratorRecord).
  2. Let done be ? IteratorComplete(result).
  3. If done is true, return false.
  4. Return result.

IteratorNext(§7.4.4)

  1. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]).
  2. If result is not an Object, throw a TypeError exception.
  3. Return result.

中文解读

IteratorNext 调用缓存的 [[NextMethod]](而不是每次重新查找),并要求返回值必须是对象。如果 next() 返回 null42"string" 等原始值,会立即抛出 TypeError。这是自定义迭代器的常见错误:

// 错误的迭代器实现
const badIterator = {
  [Symbol.iterator]() { return this },
  next() {
    return true  // ❌ 返回原始值!TypeError: Iterator result true is not an object
  }
}

// 正确的迭代器实现
const goodIterator = {
  [Symbol.iterator]() { return this },
  _count: 0,
  next() {
    return this._count < 3
      ? { value: this._count++, done: false }  // ✓ 返回对象
      : { value: undefined, done: true }        // ✓ 返回对象
  }
}

for...of 完整规范执行步骤

ECMAScript 规范中 for...of 语句(for ( LeftHandSideExpression of AssignmentExpression ))的运行时语义 ForIn/OfHeadEvaluationForIn/OfBodyEvaluation

ForIn/OfHeadEvaluation(简化)

  1. Let exprRef be the result of evaluating AssignmentExpression.
  2. Let exprValue be ? GetValue(exprRef).
  3. If iterationKind is enumerate, ...(for-in 路径)
  4. Else, let iteratorRecord be ? GetIterator(exprValue, iterationKind).
  5. Return iteratorRecord.

ForIn/OfBodyEvaluation(简化)

  1. Repeat, a. Let nextResult be ? IteratorStep(iteratorRecord). b. If nextResult is false, return NormalCompletion(V). c. Let nextValue be ? IteratorValue(nextResult). d. Perform IteratorBindingInitialization of lhs with nextValue. e. Let result be the result of evaluating stmt. f. If LoopContinues(result, labelSet) is false, return ? IteratorClose(iteratorRecord, UpdateEmpty(result, V)). g. If result.[[Value]] is not empty, set V to result.[[Value]].

步骤 f 是关键:当循环体内发生 break、return、throw(LoopContinues 返回 false)时,规范调用 IteratorClose,这触发迭代器的 return() 方法,完成资源清理。

IteratorClose 规范算法

7.4.7 IteratorClose ( iteratorRecord, completionRecord )

  1. Let iterator be iteratorRecord.[[Iterator]].
  2. Let innerResult be GetMethod(iterator, "return").
  3. If innerResult.[[Type]] is normal, then a. Let return be innerResult.[[Value]]. b. If return is undefined, return ? completionRecord. c. Set innerResult to Call(return, iterator).
  4. If completionRecord.[[Type]] is throw, return ? completionRecord.
  5. If innerResult.[[Type]] is throw, return ? innerResult.
  6. If innerResult.[[Value]] is not an Object, throw a TypeError exception.
  7. Return ? completionRecord.

中文解读

步骤2:查找 return 方法时,如果不存在(步骤3b),直接跳过,不报错——return() 方法是可选的。

步骤4-5:错误处理的优先级——如果调用者已经在处理一个 throw(比如 for...of 体内抛出了异常),则优先传播原始错误;只有在没有原始错误时,return() 的错误才会被传播。

步骤6:如果 return() 方法返回了原始值,抛出 TypeError——return() 也必须返回对象。

生成器规范实现

生成器函数调用时返回一个 Generator 对象,规范将其内部状态定义为:

A Generator object is created by invoking a generator function. It is a suspended function execution that can be resumed.

Generator objects have the following internal slots:

  • [[GeneratorState]]suspended-start | executing | suspended-yield | completed
  • [[GeneratorContext]]:执行上下文(包含局部变量、暂停点)
  • [[GeneratorBrand]]:标识符(用于区分不同类型的生成器)

GeneratorNext(§27.4.3.2)

  1. Return ? GeneratorResume(generator, value, empty).

GeneratorResume

  1. Let state be generator.[[GeneratorState]].
  2. If state is completed, return CreateIterResultObject(undefined, true).
  3. Assert: state is suspended-start or suspended-yield.
  4. Let genContext be generator.[[GeneratorContext]].
  5. Let methodContext be the running execution context.
  6. Suspend methodContext.
  7. Set generator.[[GeneratorState]] to executing.
  8. Push genContext onto the execution context stack; genContext is now the running execution context.
  9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it.
  10. Assert: When we reach this step, genContext has already been removed from the execution context stack and methodContext is the currently running execution context.
  11. Return result.

中文解读:这里揭示了生成器的核心机制:生成器保存的不是普通数据,而是整个执行上下文(Execution Context),包括当前位置(PC 指针)、局部变量、作用域链。每次 next() 调用时,当前执行上下文被暂停,生成器的执行上下文被恢复推入栈顶。这就是为什么生成器能在 yield 处"冻结"并在完全相同的状态下恢复。

Symbol.iterator 的实现细节

规范定义了内置可迭代对象的 @@iterator 方法行为:

Array.prototype[@@iterator](§23.1.3.14)

Returns an ArrayIterator object whose [[ArrayIteratorNextIndex]] is 0 and [[ArrayIterationKind]] is value.

数组迭代器在创建时记录了当前数组和起始索引,迭代期间修改数组会影响后续的迭代结果:

const arr3 = [1, 2, 3, 4, 5]
const iter2 = arr3[Symbol.iterator]()

console.log(iter2.next())  // { value: 1, done: false }
arr3.splice(2, 0, 99)      // 在索引2处插入99,arr3 = [1, 2, 99, 3, 4, 5]
console.log(iter2.next())  // { value: 2, done: false }
console.log(iter2.next())  // { value: 99, done: false }(迭代到了插入的元素!)
console.log(iter2.next())  // { value: 3, done: false }

Map.prototype[@@iterator](§24.1.3.11)Set.prototype[@@iterator](§24.2.3.10) 遵循"插入顺序"迭代,迭代期间增加/删除元素:

const map = new Map([[1, 'a'], [2, 'b'], [3, 'c']])
const mapIter = map[Symbol.iterator]()

console.log(mapIter.next())  // { value: [1, 'a'], done: false }
map.set(4, 'd')              // 添加新元素
console.log(mapIter.next())  // { value: [2, 'b'], done: false }
map.delete(3)                // 删除元素
console.log(mapIter.next())  // { value: [4, 'd'], done: false }(3 被跳过!)
console.log(mapIter.next())  // { value: undefined, done: true }

Level 4 · 边界与陷阱(全体适用)

陷阱1:生成器 return() 的资源清理时机

function* resourceGenerator() {
  const resource = acquireResource()
  try {
    yield* processItems(resource)
  } finally {
    releaseResource(resource)  // 这里会被调用吗?
  }
}

const gen3 = resourceGenerator()
gen3.next()  // 开始执行,获取资源

// 情况1:正常完成
for (const item of gen3) {}  // finally 会执行

// 情况2:提前 break
for (const item of gen3) {
  break  // for...of 调用 gen3.return(),触发 finally
}

// 情况3:直接丢弃迭代器(不 break)
const gen4 = resourceGenerator()
gen4.next()
// gen4 被垃圾回收,finally 永远不会执行!
// JavaScript 的 GC 不调用 return()

// 正确的资源管理:显式调用 return() 或用 try...finally 包裹 for...of
try {
  for (const item of resourceGenerator()) {
    if (shouldStop) break  // break 会触发 return()
  }
} catch (e) {
  // 即使抛出异常,for...of 也会调用 return()
}

陷阱的本质:如果直接操作迭代器(不用 for...of)并且在完成之前丢弃引用,return() 不会被自动调用。只有 for...of、数组解构、Array.from 等语法才会在提前终止时自动调用 return()

陷阱2:for...of 中修改集合

// 陷阱:for...of 遍历 Set 时删除已访问的元素
const set = new Set([1, 2, 3, 4, 5])
for (const item of set) {
  if (item % 2 === 0) {
    set.delete(item)  // 删除偶数
  }
  console.log(item)
}
// 输出:1, 2, 3, 4, 5(所有元素都被访问!)
// 删除操作生效,但迭代器在删除时已经获取了下一个元素的引用
// Set 迭代器按"插入顺序"工作,删除已访问的元素不影响后续迭代

// 对比:添加新元素
const set2 = new Set([1, 2, 3])
for (const item of set2) {
  if (item === 1) set2.add(4)  // 在迭代中添加
  console.log(item)
}
// 输出:1, 2, 3, 4(4 被迭代到了!因为在迭代前就被添加)

陷阱3:异步迭代中的并发问题

// 陷阱:并发调用异步迭代器的 next()
async function* asyncCounter() {
  for (let i = 0; i < 5; i++) {
    await new Promise(r => setTimeout(r, 10))
    yield i
  }
}

// 错误:同时发起多个 next() 调用
const iter3 = asyncCounter()
const p1 = iter3.next()  // 第1次 next()
const p2 = iter3.next()  // 第2次 next()(在第1次 await 期间!)
const p3 = iter3.next()  // 第3次 next()

// 规范允许这种行为,但生成器内部只有一个执行上下文
// 结果:p1, p2, p3 会按顺序解析,不会并发执行生成器体
// 这不是真正的并发,而是队列化的顺序调用
// 正确:使用 for await...of 保证顺序
async function processAll() {
  for await (const value of asyncCounter()) {
    // 每次 await 后才请求下一个值
    console.log(value)
  }
}

陷阱4:迭代器协议的 Symbol.iterator 自引用

// 规范要求:迭代器自身也应该是可迭代的
// 即 iterator[Symbol.iterator]() 应该返回 this

// 这使得迭代器可以直接用于 for...of:
function* gen5() { yield 1; yield 2; yield 3 }
const iter4 = gen5()
iter4.next()  // 消费第一个值

// iter4 此时是迭代器,也是可迭代对象
for (const x of iter4) {
  console.log(x)  // 2, 3(从当前位置继续,而不是重头开始)
}
// 如果自定义迭代器没有实现 [Symbol.iterator]() 返回 self:
const brokenIter = {
  _count: 0,
  next() {
    return this._count < 3
      ? { value: this._count++, done: false }
      : { value: undefined, done: true }
  }
  // 没有 [Symbol.iterator]!
}

for (const x of brokenIter) { }  // TypeError: brokenIter is not iterable
// 修复:添加 [Symbol.iterator]() { return this }

陷阱5:Generator.prototype.return() 的行为细节

// 生成器的 return() 方法与普通迭代器的 return() 行为有差异
function* gen6() {
  try {
    yield 1
    yield 2
    yield 3
  } finally {
    yield 4  // finally 块中的 yield 会阻止 return() 立即完成!
    yield 5
  }
}

const g2 = gen6()
console.log(g2.next())    // { value: 1, done: false }
console.log(g2.return(9)) // { value: 4, done: false }(不是 { value: 9, done: true }!)
// 因为 finally 块中有 yield,生成器继续执行到 finally 中的 yield
console.log(g2.next())    // { value: 5, done: false }
console.log(g2.next())    // { value: 9, done: true }(现在才返回 return 传入的值)

这个行为在规范的 GeneratorReturn 算法中定义,它向生成器的恢复点传递一个 return(value) 完成记录,生成器在执行到 finally 后才传播这个完成记录。

实现完整的自定义迭代器

// 生产级别的自定义可迭代对象实现
class Range {
  constructor(start, end, step = 1) {
    this.start = start
    this.end = end
    this.step = step
  }

  [Symbol.iterator]() {
    let current = this.start
    const { end, step } = this

    return {
      next() {
        if (current < end) {
          const value = current
          current += step
          return { value, done: false }
        }
        return { value: undefined, done: true }
      },

      return(value) {
        // 清理资源(如果有的话)
        console.log(`Range iterator closed at ${current}`)
        return { value, done: true }
      },

      // 使迭代器自身也可迭代
      [Symbol.iterator]() { return this }
    }
  }
}

const range2 = new Range(1, 10, 2)

// 展开
console.log([...range2])  // [1, 3, 5, 7, 9]

// for...of
for (const n of range2) {
  if (n > 5) break  // ← 触发 return()
}

// 解构
const [first, second] = range2

// Array.from
const arr4 = Array.from(range2)

本章小结

  1. 迭代器协议由两层组成:可迭代协议([Symbol.iterator]())和迭代器协议(next()/return()/throw())。前者是"进入迭代"的入口,后者是"推进迭代"的引擎;return() 不是可选的便利功能,而是资源管理的关键。

  2. for...of 的规范路径是三步:GetIterator(获取迭代器)→ 循环调用 IteratorStep(推进迭代)→ 提前终止时调用 IteratorClose(清理资源)。任何导致循环提前退出的操作(break、return、throw)都会触发 IteratorClose。

  3. 生成器函数的本质是执行上下文工厂:每次调用生成器函数返回一个保存了完整执行上下文的 Generator 对象,yield 暂停时整个调用栈被冻结(含局部变量),next() 调用时栈被恢复。这使生成器成为实现协程的最低成本方式。

  4. 内置可迭代对象的迭代是实时的:迭代 Array、Map、Set 期间修改集合会影响迭代结果,Array 迭代器使用当前数组长度检查,Map/Set 迭代器按插入顺序,迭代中添加元素会被迭代,删除已经"通过"的元素不影响剩余迭代。

  5. 异步迭代是对同步迭代的最小化扩展[Symbol.asyncIterator]()next() 返回 Promise,for await...of 在每次迭代步骤间 await。如果对象没有 [Symbol.asyncIterator],引擎会自动用 CreateAsyncFromSyncIterator 将同步迭代器包装为异步迭代器。

本章评分
4.7  / 5  (3 评分)

💬 留言讨论