js中的迭代器(Iterator)

6,448

前言

JavaScript提供了四种数据集合,分别是array、object、map和set。这四种数据集合的数据结构各不相同,但是都可以被循环遍历,这一切的背后都离不开iteration(迭代器)的支撑。遍历器(Iterator)是一种机制,也可以说是一种接口,它为各种不同的数据结构提供了统一的访问机制。任何数据结构只要配置了 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)

1、什么是iteration

iteration成为迭代器,又叫遍历器。它的作用是给不同的数据结构提供统一的遍历访问机制,这种机制可以使得数据结构里的成员按照顺序依次被遍历访问,最常见的就是数组、map的遍历了。比如

const arr=['a','b','c'];
for (let i of arr){
console.log(i)
//a、b、c
}
const map=new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
for (let j of map){
console.log(j)
//[1, 'x'], [2, 'y'], [3, 'z']
}

你应该感到好奇,这不就是数组和map数据的for...of循环嘛,我会用啊,这和iteration有什么关系?如果你真的有这个疑问,说明你对iteration确实并不是那么了解,应该看一下这篇文章。其实for...of循环的背后调用的正是iteration,换句话说,因为数组对象上部署了iteration属性,数组是一个可迭代对象,因此数组可以被for...of遍历,文章后面会再介绍相关内容。 通过上面例子我们得到两个信息:

  1. 数组、map数据结构都能使用for...of遍历
  2. 遍历的过程都是按照特定的元素排列顺序,在前面的元素先被遍历 其实iteration有三个作用:
  3. 为各种数据结构,提供一个统一的、简便的访问接口;
  4. 使得数据结构的成员能够按某种次序排列;
  5. 主要供for...of消费 iteration工作原理是什么呢?其实iteration遍历的时候会先生成一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。这个对象里面有一个next方法,然后调用该next方法,移动指针使得指针指向数据结构中的第一个元素。每调用一次next方法,指针就指向数据结构里的下一个元素,这样不断的调用next方法就可以实现遍历元素的效果了!另外每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束,没有结束返回false,结束为true。 根据iteration的工作原理,我们能够自己手写一个iteration。手写的iteration无非就是返回一个指针对象,指向数据的起始的位置,然后对象里面还应该有next方法,该方法会返回一个带有value和done属性的对象。那么让我们来实现吧:
function myIteration(arr){
    var index=0;
    return {
        next:function(){
            return index<arr.length ? 
            {value:arr[index++],done:false}:
            {value:undefined,done:true}
        }
    }
}
var test=myIteration([1,2])
console.log(test.next()) //{ "value": 1, "done": false }
console.log(test.next()) //{ "value": 2, "done": false }
console.log(test.next()) //{ "value": undefined, "done": true }

代码中myIteration是一个遍历器生成函数,执行这个函数返回一个带有next方法的遍历器对象。然后再调用对象上的next函数,通过内部索引index实现数组的遍历。如果参数arr不是数组而是一个map或者set对象也是类似的,这样就能实现map、set的迭代器了!所以说iteration为各种不同的数据结构提供了统一的访问机制。

2、原生及自定义的iteration接口

我们在js中遍历数组的时候并没有写什么myIteration函数,这是因为js已经对数组内置了这个接口。当我们使用for...of循环遍历数组数据结构时,该循环会自动去寻找 Iterator 接口并执行遍历操作。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。因此我们可以把原来的非可遍历数据改造成遍历数据。

ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数,其实就是我们上面写的myiteration函数,执行这个函数,就会返回一个带有next方法的遍历器对象。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。

在js里原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象 因此,上面的任意一个数据类型都支持for...of遍历,也可以调用自身的symbol.iteration方法,举个🌰:
let arr = ['a', 'b', 'c'];
//调用原生Symbol.iterator方法,返回一个遍历器对象
let iter = arr[Symbol.iterator]();
//调用遍历器上面的next方法,返回一个代表当前成员的信息对象
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

当然,我们还可以巴原来不是可迭代的数据类型变成可迭代的,这样就能支持for...of循环啦,比如说objct!如果我们直接对对象执行for...of肯定会出错:

let obj={
  'name':'前端小鹿',
  'age':'18',
  'sex':'男'
}
for (let i of obj){
  console.log(i) //obj is not iterable,obj不是可迭代对象
}

//对obj改造
let obj={
  data:['name:前端小鹿','age:18', 'sex:男'],
  [Symbol.iterator]:function(){
    const self=this
      let index=0;
      return {
          next:function(){
            if (index < self.data.length) {
              return {
                value: self.data[index++],
                done: false
              };
        }
            return { value: undefined, done: true };
          }
      }
  }
}
for (let i of obj){
  console.log(i) 
  //"name:前端小鹿" "age:18" "sex:男"
}

芜湖~ 起飞。通过iteration的原理,我们把对象改造成了可迭代对象,成功用for...of实现了循环,nice!

对于类数组对象,它的改造更加容易,我们直接引用数组的Iterator接口就好了,还是举个清晰的🌰:

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

这里需要注意一点,这种直接引用数组的Iterator接口方式只适用于类数组对象哦,普通对象不生效,还是乖乖用上面Symbol.iterator属性设置吧。

3、iteration接口什么时候被使用

我们上面已经介绍过了for...of会使用Symbol.iterator方法,当然还有其他的场合会用到。

  • 解构赋值 对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
  • 扩展运算符(...)
var str = 'hello';
[...str] //  ['h','e','l','l','o'],string上有iteration,因此字符串也可以用for...of来循环
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
  • generator函数 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是返回一个遍历器对象
let myIterable = {
  [Symbol.iterator]: function* () {
    yield 'a';
    yield 'b';
    yield 'c';
  }
};
console.log([...myIterable]) // [a, b, c]

for (let i of myIterable){
  console.log(i)  // [a, b, c]
}

还有其他方法背后也会用到iteration,比如Array.from()、Promise.all()等等,这里不再展开描述了。

4、总结

总之,一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,这个接口就可以被for...of循环消费,从而遍历它的成员。我们理解iteration的原理可以更好使用js提供的数据结构,必要时还可以改造不可迭代的数据结构。