ES6+知识点汇总(9)— Iterator & for...of

57 阅读6分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第15天,点击查看活动详情

十六、Iterator

16-1、什么是Iterator

Iterator被称为遍历器 / 迭代器,就是用来遍历的。

那么Iterator存在在哪里呢?

我们可以打印出一个数组来看下:

console.log([1, 2, 3]);

我们可以看到其原型链上有一个属性为Symbol.iterator的方法,那么我们可以获取到该方法打印出来:

console.log([1, 2, 3][Symbol.iterator]);

而当我们调用此方法时,可看到会输出一个数组的Iterator对象,其中包含了next方法:

console.log([1, 2, 3][Symbol.iterator]());

16-2、使用Iterator

const it = [1, 2][Symbol.iterator]();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: undefined, done: true}
console.log(it.next()); // {value: undefined, done: true}

我们可以调用Iterator的next方法,可以看出来会返回一个对象,其中包含了value属性以及done属性:

  • value属性表示调用next方法所遍历出的内容
  • done属性表示是否遍历完成,当遍历完成时,value会为undefined,done为true

总结:

通过Symbol.iterator(可遍历对象的生成方法)获取到it (可遍历对象)然后通过it.next()遍历数据直至返回的对象中done为true的过程称为Iterator


16-3、为什么需要Iterator遍历器

既然我们可以通过for / forEach来遍历数组、for...in来遍历对象,那么为什么还需要Iterator呢?况且写起来那么麻烦,还得去一个个去调用next方法并手动判断有无遍历结束。

是因为:由于遍历数组与对象我们要用不同的方法,而Iterator的出现就是为了统一遍历的方式的(不管是数组还是对象都可以统一通过Iterator进行遍历),但是需要注意的是:

  • 数组天生就可以使用Iterator进行遍历,因为其原型链上本身就存在Symbol.iterator
  • 而对象原型链上没有Symbol.iterator,没有办法直接通过Iterator去遍历,但是我们可以人为使得对象可以通过Iterator去遍历

16-4、如何更方便的使用Iterator

通过以上讲解我们得知,如果我们要想使用Iterator去遍历需要手动调取next方法并判断,那么我们是否可以更方便的使用Iterator呢?

我们可以借助while循环,来自动化实现Iterator的遍历:

    const arr = [1, 2, 3];
    const it = arr[Symbol.iterator]();
    let next = it.next();
    console.log(next);
    while (!next.done) {
      next = it.next();
      console.log(next);
    }

所以,我们一般不会直接使用Iterator去遍历,ES6已经为我们封装好了底层利用Iterator去实现的遍历方法:for...of,下面我们就来学习一下for...of吧!

十七、for...of

ES5针对数组和对象有不同的遍历方法,但这些方法或多或少会存在一定的问题。为了统一解决这些问题,ES6给出了终极解决方案:for...of。

for...of会对可迭代对象上创建一个迭代循环,它不限于数组的循环,只要是可迭代的对象就可以被for...of进行循环,包括:

  • 内置的String、Array
  • 类数组对象(arguments、NodeList)
  • Map、Set
  • 以及用户自定义的可迭代对象

17-1、for...of的基本使用方法

语法:

for (const iterator of iterable) {
  // 执行语句
}

参数描述:

参数描述
iterator在每次迭代中,将不同属性的值分配给变量,用于循环中语句的使用
iterable被迭代的可枚举对象
  1. 迭代Array
let arr = [10, 20, 30];

for (let value of arr) {
    value += 1;
    console.log(value);
}
// 11
// 21
// 31
  • 上面代码对遍历出的元素进行了+1,如果想遍历出的元素不被修改可以写成:for (const value of arr)
  1. 迭代字符串

使用for...of迭代字符串,迭代后的结果是将字符进行分隔,得到一个个单个字符:

const str = '你好';

for (const value of str) {
    console.log(value);
}
// 你
// 好
  1. 迭代Set、Map
const setArr = new Set([1, 1, 2, 2, 3, 3]);

for (const value of setArr) {
  console.log(value);
}
// 1
// 2
// 3
  • 要注意的是,Set会对数组进行去重,所以迭代出的元素是去重后的
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);

for (let value of map) {
  console.log(value);
}
// ["a", 1]
// ["b", 2]
// ["c", 3]
  • 迭代的Map是一个个包含键值对的数组,所以我们可以将数组解构进而获取出来key和value:
for (let [key, value] of map) {
  console.log(key, value);
}
// a 1
// b 2
// c 3
  1. 迭代类数组对象
  • 迭代arguments
function argfn() {
  for (let argument of arguments) {
    console.log(argument);
  }
}
argfn(1,2,3)
// 1
// 2
// 3
  • 迭代NodeList
//注意:这只能在实现了NodeList.prototype[Symbol.iterator]的平台上运行
let ps = document.querySelectorAll("p");

for (let value of ps) {
  value.classList.add("a");
}

17-2、for...of结合 break / continue

比如我们想遍历数组时如果遇到数字“2”便终止遍历:

const arr = [1, 2, 3];
for (const item of arr) {
  if (item === 2) {
    break;
  }
  console.log(item);
}
  • 可以结合break来实现,此时只会打印出数字1,因为在遇到2后便终止了循环,不会再继续

那如果我们只是想跳过“2”来打印出 1 和 3 呢?此时我们可结合continue来实现:

const arr = [1, 2, 3];
for (const item of arr) {
  if (item === 2) {
    continue;
  }
  console.log(item);
}

17-3、如何在for...of中取到数组的索引

我们可以通过数组.keys()来获取到索引的可遍历对象,然后再通过for...of去遍历该对象:

const arr = [1, 2, 3];
for (const key of arr.keys()) {
  console.log(key); // 0 1 2
}

当然,我们也可以通过数组.entries()来获取到数组的索引与值的一个个键值对,同时遍历出数组的索引与值:

const arr = [1, 2, 3];
for (const entries of arr.entries()) {
  console.log(entries);
}
// 我们可以结合解构的语法来快速获取到索引与值
for (const [index, value] of arr.entries()) {
  console.log(index, value);
}
  • 我们可以结合解构的语法来解构出数组的索引与值

17-4、原生可遍历 & 非原生可遍历

怎么判断是否可遍历呢?

  • 只要有Symbol.iterator方法,并且这个方法可以生成可遍历对象,那么就是可遍历的

17-4-1、原生可遍历

原生可遍历的有:数组、字符串、Set、Map、arguments、NodeList等

这些原生可遍历的结构可直接通过for...of来进行遍历

17-4-2、非原生可遍历

对象就是非原生可遍历的,其没有Symbol.iterator方法,默认情况下不可直接使用for..of,会报错:

xxx is not iterable。

  • 我们一种方式是使用for..in
  • 另一种方式是可以手动为其添加Symbol.iterator方法(一般在工作中不会使用,只是帮助理解)
const person = { sex: 'male', age: 18 };
person[Symbol.iterator] = () => {
  let index = 0;
  return {
    next() {
      index++;

      if (index === 1) {
        return {
          value: person.age,
          done: false
        };
      } else if (index === 2) {
        return {
          value: person.sex,
          done: false
        };
      } else {
        return {
          done: true
        };
      }
    }
  };
};

for (const item of person) {
  console.log(item); // 18 male
}

17-5、for...of对比for & forEach & for...in

17-5-1、对比for

for循环语句相比for...of来说书写起来比较麻烦,每个步骤信息都需要自己手动去处理:

const numbers = [1,2,3,4,5,6,7];
for (let index = 0; index < numbers.length; index++) {
  console.log(numbers[index]);
}

17-5-2、对比forEach

数组中内置的forEach方法的致命缺点就是:不能够跳出循环,break和return都不能奏效

numbers.forEach((value) => {
  console.log(value);
});

17-5-3、对比for...in

  • for...in主要是针对对象循环而设计的,对于数组的话遍历出来的键是索引,但是在for...in循环中是以字符串作为键名的
  • for...in循环还会遍历手动添加的其他键,包括原型链上的键

总结for...of:

  • 有着同for...in一样的简洁语法,但是没有for...in的缺点
  • 不同于forEach方法,它可以与break、continue、return配合
  • 提供了遍历所有数据结构的统一操作接口