一起养成写作习惯!这是我参与「掘金日新计划 · 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 | 被迭代的可枚举对象 |
- 迭代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)
- 迭代字符串
使用for...of迭代字符串,迭代后的结果是将字符进行分隔,得到一个个单个字符:
const str = '你好';
for (const value of str) {
console.log(value);
}
// 你
// 好
- 迭代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
- 迭代类数组对象
- 迭代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配合
- 提供了遍历所有数据结构的统一操作接口