Iterator的奥秘:深入理解JavaScript的迭代机制


theme: scrolls-light

前言

最近看到一道面试题觉得很有意思,如何才能使下面的代码打印出结果

const obj = {
    a: 1,
    b: 2,
    c: 3
}
for (let v of obj) {
    console.log(v)
}

猛地看上去挺简单,但是里面其实蕴含了很多东西,想让其打印出结果就必须要了解一个概念就是迭代器Iterator,这也正是本文所要讲的,先说下答案吧。

Object.prototype[Symbol.iterator]=function(){
    return Object.value(this)[Symbol.Iterator]()
}

只需要在for of之前给对象添加Symbol.iterator属性就可以完美打印出1,2,3,这个属性是一个函数,这里巧妙借用了数组可迭代这一点完成对对象的遍历,首先对象本身会默认调用Symbol.iterator方法,那么此时函数中的this指向的是对象本身,然后使用Object.value方法取出该对象的值并组成一个数组,数组本身是有Symbol.iterator方法的,所以相当于是在遍历一个数组。听不懂?没关系,接下来才是正文部分,那就先从概念部分说起吧。

Iterator是什么

image.png

相信经验比较丰富的一些同学可能会看到过这个,在一些数组或者Map、Set中经常会看到这个属性,但在对象(Object)中就没有,下面就来一起彻底弄明白这个东西。

Iterator名为遍历器,ES6中规定遍历器是一种机制,也是一种接口,为各种不同的数据结构提供统一的访问机制,任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。原生具备Iterator的数据结构有

  • Array
  • Map
  • Set
  • String
  • arguments对象
  • NodeList对象

为什么需要Iterator

在Javascript中表示集合的数据结构有数组、对象、Map和Set数据结构,它们都有各自的特点,但同时也有一个共同的需求:遍历它们的内容。然而不同的数据结构都有对应的遍历方法,比如,数组有forEach、map、filter等方法,对象有for in/Object.keys()方法,Map和Set也都有各自的遍历方法,这就导致一个问题,如果要处理一个不同类型的集合,那么就要写很多的判断条件来根据集合的类型选择对应的遍历方法,这显然是不方便的。

于是ES6中为了解决这个问题引入了Iterator这种统一的访问机制,即for of循环,使用for of循环时会自动去寻找该数据结构是否部署了Iterator接口。这就是为什么会出现Iterator的原因,总结一下:

  1. 为各种数据结构,提供一个统一的、简便的访问接口;
  2. 使得数据结构的成员能够按某种次序排列(自定义遍历行为);
  3. for of使用;

Iterator的实现

Iterator其实就是一个对象,对象中有一个next方法(固定格式),第一次调用对象的next方法可以返回当前数据结构中的第一个成员,第二次调用对象的next方法可以返回当前数据结构中的第二个成员,直到数据结构中的结束位置结束。下面来模拟实现一个遍历器生成函数:

const arr = [1,2,3];
function makeIterator(arr){
    let nextIndex = 0;
    return {
        next(){
               return nextIndex < arr.length?
               {value:arr[nextIndex++],done:false}:
               {value:undefined,done:true}
        }
    }
}

const iterator = makeIterator(arr);
console.log(iterator.next()) // { value: 1, done: false }
console.log(iterator.next()) // { value: 2, done: false }
console.log(iterator.next()) // { value: 3, done: false }
console.log(iterator.next()) // { value: undefined, done: true }

可以看出调用遍历器对象的next方法,就可以遍历事先给定的数据结构。在makeIterator函数中首先声明了一个表示从头开始的下标,每调用一次next方法就会以对象的形式返回数据结构中对应的元素,其中value表示数据结构中当前位置的值,done表示遍历是否结束。

上面提到的Symbol.iterator属性,它本身是一个函数,是当前数据结构默认的遍历器生成函数,这个函数的实现类似于makeItrerator函数,它默认部署在可遍历的数据结构中。

只要数据结构具有Symbol.iterator属性就可以使用for of遍历,for...of循环内部调用的其实就是数据结构的Symbol.iterator方法。

const arr = [1,2,3,4];
for (let v of arr){
    console.log(v) // 1 2 3 4 
}

Object对象上的没有默认的Iterator接口,是因为对象是无序的,里面的哪个属性先遍历哪个属性后遍历是不确定的,需要开发者手动确定。要使其可以进行遍历,添加一个遍历器生成函数即可。

const obj = {
    name:"xxx",
    age:18,
}
// 如果不加此函数,将会报错 obj is not iterable
Object.prototype[Symbol.iterator] = function(){
    let nextIndex = 0
    return {
        next:()=>{ // 确定this指向
            return nextIndex < Object.values(this).length?{value:Object.values(this)[nextIndex++],done:false}:{value:undefined,done:true}
        }
    }
}
for (let v of obj){
    console.log("v",v) // xxx 18
}

此外其他默认调用Iterator接口的场景还有解构赋值、扩展运算符。再补充一下for in for of是两种不同的数据结构,分别用于遍历两种不同的数据结构,for in 枚举的对象的键,而不是值,for of直接遍历的是对象的值,而不是键。

总结

这篇文章主要讲解了JavaScript中一个可迭代对象是如何进行迭代的,详细说明了Iterator遍历器的概念、出现的原因以及如何实现,属于原理级别的知识,虽然在工作中不会经常用到,但是本着知其然知其所以然的道理了解它的原理,内部是如何运行的才能知道一些报错信息出现的原因以及解决方案,大家加油吧!


这是一个从 https://juejin.cn/post/7368767452614672419 下的原始话题分离的讨论话题