for of 迭代原理,超简单!

354 阅读7分钟

迭代器(Iterator)

迭代器是借鉴C++等语言的概念,迭代器的原理就像指针一样,它指向数据集合中的某个元素,你可以获取它指向的元素,也可以移动它以获取其它元素。

JS中的迭代器则是专门为了遍历这一操作设计的。每次获取到的迭代器总是初始指向第一个元素,并且迭代器只有next()一种行为,直到获取到数据集的最后一个元素。我们无法灵活移动迭代器的位置,所以,迭代器的任务,是按顺序遍历(读取)数据集中的元素

迭代器是一个对象,JS规定,迭代器必须实现next()接口,它应该返回当前元素并将迭代器指向下一个元素,返回的对象格式为{ value: 元素值, done: 是否遍历结束 },其中,done是一个布尔值。

我们试着来实现一个迭代器:

const iterator = { // 迭代器是一个对象,先创建一个对象
    i: 0,
    next() { // 实现next()接口
        if (this.i > 10) return {value: undefined, done: true };
            return { value: this.i++, done: false };// this.i++ 将迭代器指向下一个元素
    }
}

//手动使用迭代器
console.log(iter.next());  //{ value: 0 }
console.log(iter.next());   //{ value: 1 }

// 自动调用迭代器
while (true) {     
    let item = iterator.next();
    if (!item.done) {
        console.log(item.value);     //打印从2到10
    } else {
        break;
    }
}

这是一个符合规定的迭代器,但是把它单独拿出来并没有什么意义,迭代器的意义是附着在对象上,让一个对象,或者数据结构成为可迭代对象。

迭代器接口与可迭代对象

迭代器接口是我们获取对象的迭代器时默认调用的接口,一个实现了迭代接口的对象即是可迭代iterable对象。

对象的迭代器接口(方法)被挂在@@iterator属性上,例如Array.prototype[@@iterator](), 当需要对一个对象进行迭代时(比如开始用于一个for..of循环中),它的@@iterator方法都会在不传参情况下被调用,返回迭代器用于获取要迭代的值。

一些内置类型拥有默认的迭代器行为,其他类型(如 Object)则没有。下面的内置类型拥有默认的@@iterator方法,即它们是内置的可迭代对象:

Symbol.iterator则指向了对象的迭代器接口([@@iterator]()),调用时也会返回迭代器,[Symbol.iterator](),可以用它获取或设置对象的迭代器。

我们来尝试获取一下数组的迭代器:

arr = [1, '2', 3];
let arrIt = arr[Symbol.iterator](); // 获取数组迭代器,Symbol.iterator要使用[Symbol.iterator]包裹起来
console.log(arrIt.next());   //{ value: 1, done: false }
console.log(arrIt.next());  //{ value: '2', done: false }
console.log(arrIt.next());   //{ value: 3, done: false }
console.log(arrIt.next());  //{ value: undefined, done: true }

我们看到成功调用了next()方法,说明已经获取了数组的迭代器。

实际上数组的@@iterator 属性和 Array.prototype.values()属性的初始值是同一个函数对象。 即arr[Symbol.iterator]会返回values() 函数,而values()函数则返回一个迭代器。

image.png

image.png

是不是恍然大明白!

自己实现一个数组的迭代器:

const arr = [10, 20, 30];
// 数组迭代器对象
const arrIterator = {
  i: 0,
  next() {
    if (this.i < arr.length) {
      return { done: false, value: arr[this.i++] };
    }
    return { done:true, value: undefined };
  }
}

console.log(arrIterator.next()); // {done: false, value: 10}
console.log(arrIterator.next()); // {done: false, value: 20}
console.log(arrIterator.next()); // {done: false, value: 30}
console.log(arrIterator.next()); // {done: true, value: undefined}

甚至你可以更改数组的原生遍历器

const arr = [10, 20, 30];
// 数组迭代器对象
const arrIterator = {
  i: 0,
  next() {
    if (this.i < arr.length) {
      return { done: false, value: arr[this.i++]+1 };
    }
    return { done:true, value: undefined };
  }
}

arr[Symbol.iterator] = () => arrIterator; // 创建一个迭代器接口(即方法)返回迭代器对象,注意迭代器接口和迭代器对象的区别
// 或
Array.prototype[Symbol.iterator] = () => arrIterator; // 这种方式会改掉所有数组的迭代器

for (const item of arr) {
    console.log(item); // 11 21 31
}

但是,别瞎改,这没有意义。

迭代器的作用

  1. 为各种数据结构,提供一个统一的、简便的访问接口;

    实现这个接口的数据结构都可被遍历,即遍历(访问)数据结构的统一的一种方法。

  2. 使得数据结构的成员能够按某种次序排列;

    将你定义的数据结构可以以有序的方式被读取,例如你定义的[1, 2, 3],展示到页面时也是按顺序的。

  3. ES6 创造了一种新的遍历命令for…of循环,Iterator 接口主要供for…of消费。

自定义可迭代对象

根据迭代器的作用,意味着我们可以把非可迭代对象变为可迭代对象,例如:

我们开头举例的迭代器就是一个对象,有自己的属性,它不可迭代,我们来改一下

const iterator = { // 迭代器是一个对象,先创建一个对象
    i: 0,
    next() { // 实现next()接口
        if (this.i > 10) return {value: undefined, done: true };
            return { value: this.i++, done: false };// this.i++ 将迭代器指向下一个元素
    }
    
    // 实现对象的迭代器接口,然后它就可以被遍历了
    [Symbol.iterator]() {
        return this; // 因为这个对象本身就是迭代器对象,所以可以用自身作为自己的迭代器,你也可以自己写迭代器
    }
}

for (const item of iterator) {
    console.log(item); // 0,1,2,...,10
}

给对象实现迭代器接口后就可以遍历对象了,类似地,你还可以给类或方法添加迭代器接口,例如:


class iterableList {
    constructor(data) {
        this.data = data;
    }

    [Symbol.iterator]() {
        ...
    }
}

const list = new iterableList([]);

for(let item of list) {
  console.log(item);
}


可迭代对象的意义

可迭代对象作为数组的扩充,在以前,要操作一组数据,只有数组和对象这种形式,对象的能力很有限,所以大部分场景我们需要使用数组,非数组对象必须通过某种方式转化为数组,完成之后,还可能需要还原成原来的结构,这种繁琐的来回切换很不理想。有了可迭代对象的概念,可以对更多的数据结构直接进行访问遍历,而无需转化为数组,某些操作可以接受一个可迭代对象作为参数,传入数组、Set、Map以及其他任何实现了可迭代接口的对象也能正常处理。例如:

// 求和
function sumInIterable(iterable){
    let sum = 0;
    for(let num of iterable){
        sum+=num;
    }
    return sum;
}

上面的方法传入任意可迭代对象都能进行正确的处理。

数组到可迭代对象的提升,代表了方法的通用性的提升。请问你在设计方法的时候,会考虑能否使用可迭代对象代替数组吗?你知道Promise.all()他传入的参数就是可迭代对象吗,我们大多数人是不是只知道它可以传入数组呢。

Promise.all(iterable)

一句话总结:

可迭代对象作为数组的扩充,可以对更多的数据结构进行直接处理,当作为方法的参数时,可以提高方法的通用性。

消费可迭代对象的场合(调用Iterator接口)

  • for...of语法
  • ...iterable:扩展运算符和解构赋值
  • Generator yield*语法
  • MapSetWeakMap,WeakSet的构造器,比如new Map([['a',1],['b',2]])
  • Array.from(iterable)Object.fromEntries(iterable) 
  • Promise.all(iterable)promist.race(iterable)

所以,从现在起不要再认为它们接受的仅仅是数组参数啦,数组参数只是可迭代对象中的一种。

总结

迭代器(iterator)是ES6提出的给不同数据结构提供统一的访问接口的一种机制,有了它之后,可以很容易地创造出更多的数据结构,如set、Map,可以写出更通用的接口,如Promise.all(iterable)Object.fromEntries(iterable)等。

细心的小伙伴可能发现了,好多ES6+的新特性都与迭代器有关,现在应该能理解“为各种数据结构提供统一的访问接口的一种机制”这句话了吧。