迭代:一道面试题引出的一片知识真空(二)

在上一篇文章中,我们初步了解到迭代是对具有迭代器对象内部元素的顺序访问可迭代性由是否实现 Symbol.iterator 接口决定,与具体数据结构无关 —— 我们可以通过为对象添加迭代器使其可迭代,也能通过移除迭代器让数组等结构不可迭代✅。

本文将深入迭代器的内部实现,并手把手教你手写一个自定义迭代器。如果你对迭代的基础知识仍有困惑,强烈推荐先阅读本系列第一篇博文:

迭代:一道面试题引出的一片知识真空(一)

一、可迭代性与数据类型无关

突然想起高中语文课本中的一句文言文,恰能类比本节核心思想:

原文:君子生非异也,善假于物也。

译文:君子并不是生来就与众不同,只是因为他们善于借助其他东西罢了。

对应本节中,可迭代性并非数组、字符串等数据类型的「原生属性」,而是通过Symbol.iterator方法返回的迭代器实现的✅

我们把数组的迭代器去掉,它一样不可以使用 for…of 去遍历:

const arr = [1, 2, 3, 4, 5]
// ❌去掉 arr 的迭代器
arr[Symbol.iterator] = null
for(let val of arr) {
    console.log(val)  // 报错 arr 不是一个可迭代对象!
}

同理,我们也可以为普通对象添加迭代器使其支持for...of,具体实现可见上篇文章示例。

二、迭代器的使用效果

既然要实现一个自定义的迭代器,我们不妨先看看原生的迭代器使用起来是什么效果。

const arr = [1, 2, 3, 4, 5]
// 获取 arr 的迭代器
const iter = arr[Symbol.iterator]()
console.log(iter.next())  // { value: 1, done: false }
console.log(iter.next())  // { value: 2, done: false }
console.log(iter.next())  // { value: 3, done: false }
console.log(iter.next())  // { value: 4, done: false }
console.log(iter.next())  // { value: 5, done: false }
console.log(iter.next())  // { value: undefined, done: true }
  • 调用Symbol.iterator会返回一个迭代器对象,该对象包含next()方法;
  • 每次调用next()返回一个包含value(当前元素)和done(是否结束)的对象;
  • done: true时,value可为任意值(规范未强制要求,通常为undefined)。

由此我们可以整理出如下的结构伪代码:

// 解构伪代码
arr {
    ... 其他属性
    Symbol.iterator: function() {
        return {
            next: function () {
                return { value, done }
            }
        }
    }
}

下面,我们就从上面效果整理出的结构为起点,反推迭代器的实现。

三、从效果反推实现

根据上面的结构,我们知道,arr上面有一个属性 Symbol.iterator,这个属性对应一个方法,将返回一个带有next函数的对象:

const arr = [1, 2, 3, 4, 5]
arr[Symbol.iterator] = function () {
    return {
        next: function () {
        
        }
    }
}

我们需要一个指针指示当前被返回的元素,这个指针默认指向第一个元素。并且多次调用 next 方法时,该指针并没有被重置,而是基于目前位置继续向前移动,可以基本断定我们需要用到闭包来保存多次调用时对指针的引用。

同时,由于我们需要访问 arr 中的元素,但是 next 函数作用域中获取不到 arr,我们需要在 return 之前保留当前作用域的 this:

  1. 使用闭包保存迭代指针i,避免多次调用next()时指针重置;
  2. 通过this引用保存原数组,确保在闭包中能访问数组元素。
const arr = [1, 2, 3, 4, 5]
arr[Symbol.iterator] = function () {
    let i = 0  // 指针指向第一个元素
    const arrRef = this  // 保存当前作用域 this
    return {
        next: function () {
            // 代码中使用 i,保存对 i 的引用,形成闭包
        }
    }
}

接下来就很简单了,每次调用 next 时,我们需要判断指针是否已经完成了迭代,如果已经完成就返回{ value: undefined, done: true },没有完成返回 { value: arr[i], done: false }

arr[Symbol.iterator] = function () {
  let i = 0
  const arrRef = this
  return {
    next: function() {
      if (i >= arrRef.length) {
        return { value: undefined, done: true }  // 迭代结束
      } else {
        const value = arrRef[i]
        i += 1  // 指针前进
        return { value, done: false }  // 迭代未结束
      }
    }
  }
}

到此为止,我们就简单地复现了数组迭代器的内部实现,让我们看看运行出的效果和原生迭代器是否一致吧:

const arr = [1, 2, 3, 4, 5]

// 我们的迭代器将覆盖原有的迭代器
arr[Symbol.iterator] = function () {
  let i = 0
  const arrRef = this
  return {
    next: function() {
      if (i >= arrRef.length) {
        return { value: undefined, done: true }
      } else {
        const value = arrRef[i]
        i += 1
        return { value, done: false }
      }
    }
  }
}

const iter = arr[Symbol.iterator]()
console.log(iter.next())  // { value: 1, done: false }
console.log(iter.next())  // { value: 2, done: false }
console.log(iter.next())  // { value: 3, done: false }
console.log(iter.next())  // { value: 4, done: false }
console.log(iter.next())  // { value: 5, done: false }
console.log(iter.next())  // { value: undefined, done: true }

运行结果完全一致!

四、自定义一个简单的迭代器

让我们实现自定义迭代器,让数组通过for...of遍历时仅输出奇数索引元素(1, 3, 5),你知道怎么实现了吗?

const arr = [1, 2, 3, 4, 5]

for(let val of arr) {
    console.log(val)  // 要求打印 1, 3, 5
}

很简单,上面在实现迭代器的代码中,每次我们让指针 i 前进一步来迭代每一个元素。现在我们只需要让指针 i 每次前进两步就可以啦:

arr[Symbol.iterator] = function () {
  let i = 0
  const arrRef = this
  return {
    next: function() {
      if (i >= arrRef.length) {
        return { value: undefined, done: true }
      } else {
        const value = arrRef[i]
        i += 2  // 指针前进两步,跳过奇数索引元素
        return { value, done: false }
      }
    }
  }
}

for(let val of arr) {
    console.log(val)  // 打印 1, 3, 5
}

是不是很神奇呢?这也正说明 for … of 确实是基于迭代实现的遍历。

五、总结

我们基于上篇文章的迭代的理解,在本篇文章中继续深入理解迭代的实现原理,从原生的实现效果入手确定代码结构骨架,然后一点点根据目标效果对代码进行补全,最后实现了一个自定义的迭代器使 for…of的行为发生变化。

当然,回归到我们的面试题上,题目中除了对于迭代的考察之外,还似乎在有意考察数组解构对象解构的区别。

事实上虽然同为解构,但是两者的实现原理可谓大相径庭。

延伸问题(后续博文预告)

  1. 数组解构 vs 对象解构:两者的底层实现有何本质区别?
  2. 生成器函数(Generator) :如何通过更简洁的语法创建迭代器?
  3. 实际应用场景:迭代器在异步编程、数据流处理中有哪些经典用法?

接下来的文章将围绕这些问题展开,欢迎持续关注!✨

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值