JS一篇文章讲透迭代器-超好懂

460 阅读5分钟

什么是迭代

  • 循环遍历一个有限集合
  • 按照预定的顺序遍历
  • 遍历的过程与具体的结构无关
  • 有明确的的开始和结束标识

我们来看一个例子

const array = [1,2,3,4]
for(const i = 0; i < array.length; i++ ){
  console.log(array[i]);
}

这个代码遍历了一个数组,并相继输出每个元素。数组是最典型的迭代结构。在迭代的过程,顺序是确定的,数量是有限的,但是遍历之前要确定数据结构,才会开始遍历。并且访问数据的方式是与数组高度相关的--通过下标来获取元素,也没有明确的开始和结束的标识

在Java,Pathon,C++中,对迭代模式已经有了完备的支持。JavaScript在ES6之后,也支持了迭代模式

来看几个迭代的例子

for-in

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

const set = new Set().add(1).add(2).add(3);
for(const item of set){
  console.log(item);
}
//1
//2
//3

for(const item of '1234'){
  console.log(item);
}
//1
//2
//3
//4

数组、set、字符串都是可迭代结构,而for-of语法就是用来迭代可迭代结构的,所以就可以看到上面的输出

Array.from

const array = Array.from('123');
console.log(array);
//[ '1', '2', '3' ]

Array.form()接收一个可迭代结构作为参数,然后用迭代结构中的每个元素生成新的数组

Array.from()除了接受可迭代结构作为参数,还可以接受含有length属性和可索引元素的结构,不过这个不是迭代器讨论的范畴,这里跳过

new Set()

const set = new Set('123');
console.log(set);
//Set(3) { '1', '2', '3' }

const set2 = new Set(['a', 'b', 'c']);
console.log(set2);
//Set(3) { 'a', 'b', 'c' }

new Set()也接收一个可迭代结构,用于生成新的set对象。

set本身也是一个的可迭代结构

new Map()

const map = new Map([[1, 2], [12, 23]]);
console.log(map);
// Map(2) { 1 => 2, 12 => 23 }

const set = new Set().add(['a', 'b']).add(['c', 'd']);
const map2 = new Map(set);
console.log(map2);
// Map(2) { 'a' => 'b', 'c' => 'd' }

new Map()也接收一个可迭代结构作为参数,由于map结构的特殊性,要求迭代结构的每个元素需要是[key value]样的数组。但丝毫不影响它接收一个可迭代结构的本质性。在第二个例子中,传入了一个可迭代结构set,set的每个元素都是[key value],依旧可以生成map

map也是一个可迭代结构

OK,上面就是一些与迭代器有关的基本应用,看起来花里五哨的,是不是。

别怕,下面就深入迭代器了解一下

迭代器协议

迭代器返回的结构

{
  done: boolean;
  value: any
}

迭代器每次迭代的结果结构必须是上面的结构,包含done和value两个属性。

其中的done表示是否迭代结束,如果done === false,即没有迭代结束;如果done === true,即迭代结束。其中的value,是每次迭代返回的真正的值。

假设我们要遍历[1, 2, 3]数组,迭代器返回的值分别是:

// first
{
  done: false;
  value: 1;
}

// second
{
  done: false;
  value: 2;
}

//third
{
  done: false;
  value: 3;
}

//fourth
{
  done: true;
  value: undefined;
}

看到这里,你可以会有疑问,为啥数组迭代出来的是这个东西,和for-of输出结果完全不一样。

迭代器

讲到这里,就不得不提迭代器的概念了。

可迭代结构又称可迭代对象,我们要想迭代可迭代对象,就必须要使用迭代器。而这个迭代器可以从迭代对象中获取。在可迭代对象中,有个属性叫Symbol.iterator,这个属性指向一个函数,调用这个函数就会得到对应的迭代器。

迭代器的使用

迭代的过程,相当于有一个游标指着迭代对象中的元素,当迭代器读取完一个元素之后,这个游标就会指向下一个元素,直至遍历完成。

我们看代码

const array = [1, 2, 3];
const iteratorArray = array[Symbol.iterator]();

let flag = null;
do {
  console.log(flag = iteratorArray.next());
} while (flag.done === false)

console.log(iteratorArray.next());


// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }
  1. 首先,通过调用Symbol.iterator所指向的函数,我们获取到了数组的迭代器。之后通过调用迭代器的next()函数,来获取迭代对象中的元素,并且游标指向下一个。
  2. 这个过程中done的值为false;如果迭代完成,done会变成ture。其中value的值,就是迭代的元素的值。
  3. 迭代结束之后,还可以调用next,只不过没有了意义

读者:好吧,我明白迭代器怎么用了,但为啥和for-of输出不一样呢

作者: for-of结构会在后台调用可迭代对象提供的产生迭代器的函数,即Symbol.iterator。从而创建一个迭代器实现迭代。也就是说我们实际写代码的过程中,不需要显示调用这个工厂函数来生成迭代器,也不需要我们手动处理看起来怪怪的结构了--{done,value}

类似for-of的原生语言结构

for-of的原生语言结构有好多:

  • 数组解构。没错,解构的对象没必要是数组,是个可迭代对象就好了
  • 扩展操作符。这个和上面一样,数据来源是个可迭代对象就可以。
  • Array.from(), new Set(), new Map(),这些函数都会接收一个可迭代对象作为参数,生成对应的实例化对象
  • Promise.all(), Promsie.race()。这里也接收一个可迭代对象作为参数

不知道你有没有反应过来,这个迭代器的返回结果数据结构,和生产器(generator)返回的数据结构一模一样

下面对set对象做数组解构和扩展操作符的计算,其他的读者可以自己尝试

const set = new Set().add(['a', 'b']).add(['c', 'd']);
console.log(set);
// Set(2) { [ 'a', 'b' ], [ 'c', 'd' ] }

//对set做扩展操作符运算
const array = [...set];
console.log(array);
// [ [ 'a', 'b' ], [ 'c', 'd' ] ]

// 对set做数组解构运算
const [a, b] = set;
console.log(a);
console.log(b);
// [ 'a', 'b' ]
// [ 'c', 'd' ]

可以很容易猜到,数组解构和扩展操作符两种计算,都是先把迭代对象迭代一遍,获取每个元素,之后再生成对应的内容

哪些结构是可迭代的

  • 字符串
  • 数组
  • map
  • set
  • arguments对象
  • NodeList等DOM集合类型

是不是比你以为的还要少一些😁

如何看一个结构是否为可迭代结构

  • 用for-of遍历一下,如果可以遍历,那就是可迭代结构
  • 看该对象的Symbol.iterator的属性是否为undefined

下一篇文章讲自定义迭代器: JS讲透迭代器-自定义篇

总结

  1. 什么是迭代
  2. 举了几个JS中运用迭代器的例子
  3. 讲述了迭代器协议
  4. 有不明白的,留言告诉我