概述
目录
- 一、什么是遍历器
- 二、如何部署遍历器
- 三、遍历器的应用
- 1. 解构赋值
- 2. 扩展运算符
- 3. Iterator与Generator函数
- 4. return和throw
- 总结
一、什么是遍历器
在ES5中,我们最常使用表示“集合”的数据结构主要是数组(Array)和普通对象(Object),ES6在此基础上新增了Map和Set。我们知道,这些“集合”类元素都是由一系列的成员构成的,那么一个非常常见的需求就是如何依次访问“集合”中的每一个成员。
在ES5中,数组成员主要通过for循环或原型方法forEach等来遍历,而对象成员则没有方法直接遍历(for … in、Object.keys()等都是在遍历key,而不是value)。ES6为数组和普通对象,以及新增的Map和Set提供了统一的遍历机制:遍历器(Iterator),并新增了for … of语法来使用遍历器。
Array、Map和Set三类数据结构原生部署了遍历器,因此可以直接使用for … of来枚举每个成员;普通对象没有默认的遍历器,如果需要枚举对象成员,需要手动实现一个遍历器。
简单来说,遍历器是一个对象,它至少具备一个next方法,每次调用该方法可以枚举目标“集合”的一个成员。
一个遍历器对象可以适配一种特定的“集合”结构,它可以是该“集合”的一个属性,也可以不是。如果将一个遍历器部署到一个“集合”的[Symbol.iterator]属性上,那么使用for … of遍历集合属性时,实际上就是在调用该遍历器。
下面的函数接收一个数组,然后生成一个对象,调用该对象的next方法可以依次枚举数组的每个成员:
function makeIterator(arr){
let index = 0;
return {
//这里返回了一个带有next方法的对象,该对象就是一个遍历器
next(){
if(index < arr.length){
return {value: arr[index++], done: true}
} else {
return {value: undefined, done: true}
}
}
}
}
let arr = [1, 2, 3];
let iterator = makeIterator(arr); //输入数组,得到了一个遍历器对象
iterator.next();
//{value: 1, done: false}
iterator.next();
//{value: 2, done: false}
iterator.next();
//{value: 3, done: false}
iterator.next();
//{value: undefined, done: true}
makeIterator方法传入一个数组后返回了一个对象,该对象包含一个next方法。每次调用next方法,就可以输出一个形如{value: v, done: false}格式的对象,它的value属性表示当前成员,done属性表示是否遍历完毕。依次调用next方法,就可以按序输出数组的每一个成员。
如果你希望在while循环中使用遍历器自动遍历所有属性,可以把上面的四个next调用改成下面的形式:
let res = iterator.next();
//先得到第一个属性
while (!res.done) {
//根据返回值的done属性决定是否继续遍历
var x = res.value;
// ...
res = iterator.next();
}
通过判断next返回值的done属性是否为true可以知道是否应该退出循环。
我们调用上述函数时传入的是数组,这可能让人误以为这个函数只能生成适配数组的遍历器,实际上并不是这样的。比如下面的结构:
let obj = {
length: 3,
'0': 0,
'1': 1,
'2': 2,
}
显然这是一个对象。
但与一般对象不同的是,它有一个length属性,并且有由数字构成的属性名,因此它是一个类数组。将该对象传入上述函数,同样可以构造出可以遍历该对象属性的遍历器。连续调用遍历器的next方法,可以得到与之前完全一致的结果。
假如这个对象有一个额外的name属性,那么根据遍历器的规则,这个name属性不会被输出出来。因为这里的makeIterator只能生成这样规则的遍历器:先获取“集合”的length属性值n,然后依次输出obj[0], obj[1], … obj[n]。只要传入的“集合”有length属性,并且包含数字键名,就适用该规则,而超出该规则的任何属性都无法被遍历器访问到(包括length属性本身)。这也就意味着,以下的结构都可以适用这个遍历器:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
因为它们都满足有一个length属性,并且可以通过target[n]的形式访问每个成员。
所以说,遍历器就是定义了如何依次访问目标“集合”成员的一个对象,每次调用它的next方法,就可以访问到一个成员。
二、如何部署遍历器
上面我们说到,Array、Map和Set三类数据结构原生部署了遍历器,因此你可以直接通过for … of语句遍历其成员。如:
let a = [1,2,3];
for(let item of a){
//依次输出:1 2 3
console.log(item);
}
let s = new Set([1, 2, 3, 4]);
for(let item of s){
//依次输出:1 2 3 4
console.log(item);
}
let m = new Map();
m.set('name', '夕山雨');
m.set('age', 24);
for(let item of m){
//依次输出:'夕山雨' 24
console.log(item);
}
那么它们是如何部署遍历器的呢?答案可以在它们的原型对象上找到:
在控制台输出数组的原型对象,它有一个类型为Symbol,名为Symbol.iterator的方法,这就是js引擎为数组部署的原生遍历器。不过我们无法直接在控制台打印出它的实现,因为它是由其他语言实现的,但实现原理就类似于上面所写的makeIterator函数。
Set和Map的原型对象上也可以找到该方法,因此都可以使用for … of来遍历。
我们知道,Array、Set和Map的成员都有特定的顺序(ES6规定,Set和Map的成员顺序为其加入集合的顺序),但是普通对象的属性却是没有顺序的。比如下面的对象:
let author = {
name: '夕山雨',
age: 24,
stature: '179',
weight: 65
}
这个对象有四个属性。
尽管我们可能会期待js引擎按照书写顺序给这四个属性排个序,但是非常抱歉,js引擎没有遵循这样的规则 – 因此普通对象的属性并不是线性结构。尽管我们有很多方法可以间接访问对象的每个属性值(如借助for … in循环、Object.keys、Reflect.ownKeys等),但这些方法最大的问题是,它们不能按照我们希望的某个顺序来输出(这是理所当然的,你怎么能期待js引擎知道你希望的顺序是什么呢?毕竟字符串不像阿拉伯数字一样天然就是有序的)。如果我们希望对象属性以某个特定顺序输出,就必须为它定义特殊的规则,而这套规则就是借助遍历器添加到对象上的。
举个简单的例子:
let author = {
name: '夕山雨',
age: 24,
stature: '179',
weight: 65,
[Symbol.iterator](){
let sort = ['name', 'stature', 'weight', 'age'];
let index = 0;
let _this = this;
return {
next(){
return index < 4 ?
{value: _this[sort[index++]], done: false}:
{value: undefined, done: true}
}
}
}
}
我们针对该对象的特定结构,约定按照[‘name’, ‘stature’, ‘weight’, ‘age’]的顺序访问对象的属性,因此我们可以用下面的for … of语句来输出对象的属性:
for(let item of author){
console.log(item);
}
你会得到这样的输出:
仅仅为该对象添加了一个[Symbol.iterator]方法,我们就可以用for … of语句来按序访问对象的属性了,是不是很方便?
for … of默认不会输出next方法返回的原始对象,而是只保留它的value属性。这是因为js引擎会自动判断何时结束循环,不需要把done属性暴露给开发者。
当然了,实际开发中,为每一个对象单独定义一个遍历器属性实在不是一件聪明事。我们可以仿照js引擎部署遍历器的做法,将遍历器部署在对象原型上,这样由某个构造函数或类(指es6的class)所生成的每个对象都可以方便地进行遍历了。
比如:
function Student(name, age){
this.name = name;
this.age = age;
}
Student.prototype[Symbol.iterator] = function(){
let index = 0;
let keys = [...Object.keys(this)];
let _this = this;
keys.sort((key1, key2) => { //按字母顺序对属性排序
return key1 < key2 ? -1 : 1;
})
return {
next(){
return index < keys.length ?
{value: _this[keys[index++]], done: false} :
{value: undefined, done: true}
}
}
}
现在使用Student构造函数构造的所有对象都可以使用for … of来遍历每个属性值,并且遍历顺序为按照属性键在字母表中的顺序,如:
let s = new Student('小明', 24);
for(let val of s){
console.log(val);
}
//输出:24 '小明'
上述语句先输出24,后输出’小明’,这是因为’age’ < ‘name’,而我们规定要按照属性键从小到大的顺序输出。
使用class定义时也是类似的:
class Student{
constructor(name, age){
this.name = name;
this.age = age;
}
[Symbol.iterator](){
let index = 0;
let keys = [...Object.keys(this)];
keys.sort((key1, key2) => { //按字母顺序对属性排序
return key1 < key2 ? -1 : 1;
})
let _this = this;
return {
next(){
return index < keys.length ?
{value: _this[keys[index++]], done: false} :
{value: undefined, done: true}
}
}
}
}
它与构造函数版本是等价的。
这里有一个技巧:[Symbol.iterator]函数返回的不一定必须是临时对象,按照ES6的规定,它可以是任何具备next方法的对象,包括它自己,如:
let s = {
name: '小明',
age: 24,
[Symbole.iterator](){
return this;
},
next(){
//当前对象有next方法,因此[Symbole.iterator]可以直接返回当前对象
....
}
}
在ES6中,具有next方法的对象被称为iterable(可遍历的)对象,因此换句话说,[Symbole.iterator]只要求返回的对象是iterable的。如果返回的对象没有next方法,在调用for … of时就会得到这样的报错:xxx is not iterable。
三、遍历器的应用
除了上述基本用法外,Iterator遍历器在ES6中还有着广泛的用途:
1. 解构赋值
Array和Set的解构赋值就是借助遍历器来实现的,如:
const [a, b] = [1, 2];
//a: 1, b: 2
const [c, d] = new Set([3, 4]);
//c: 3, d: 4
js引擎依次在左右两侧结构上调用next方法,进行逐个赋值,这样左侧数组的每个变量会对应被赋为右侧的值。
2. 扩展运算符
ES6的扩展运算符可以将数组展开为一列,这也是借助Iterator接口实现的,如:
let args = ['name', 'age'];
f(...args);
//等价于f('name', 'age')
既然扩展运算符是借助Iterator接口来实现的,那是不是说所有具有Iterator接口的对象都可以用扩展运算符展开?
没错!
这就是说,除了像类数组(arguments、NodeList等)、Set、Map之类原生部署了遍历器的结构可以使用扩展运算符,任何实现了[Symbol.iterator]的对象都可以用扩展运算符展开(如我们上面的author对象,或者由Student生成的对象)。
f(...author);
//等价于f('夕山雨', '179', 65, 24)
是不是有点感慨Iterator的强大了?
3. Iterator与Generator函数
不得不说,Iterator与Generator函数简直就是天生一对!
为什么这么说呢?因为Generator函数调用之后返回的就是一个遍历器对象,这个对象原生就具备next接口,这意味着你可以像下面这样书写一个非常简洁的Iterator接口:
let s = {
name: '小明',
age: 24,
[Symbol.iterator]: function* (){
yield this.name;
yield this.age;
}
}
我们只用了两行代码来规定先输出name属性,再输出age属性,没有任何多余的代码,干净而优雅!
关于Generator函数,我们后面会继续探讨。
4. return和throw
遍历器对象除了必要的next方法外,还可以部署return和throw方法,用于在for … of语句中终止遍历和抛出异常。如:
let s = {
name: '小明',
age: 24,
[Symbol.iterator]: function (){
let index = 0;
let keys = ['name', 'age'];
let _this = this;
return {
next(){
return index < keys.length ?
{value: _this[keys[index++]], done: false} :
{value: undefined, done: true}
},
return(){
...
//结束循环前可以在这里执行某些操作,如关闭文件系统等
return {done: true}
},
throw(){
...
//抛出异常时可以在这里执行某些操作
return {done: true}
}
}
}
}
for(let val of s){
console.log(val);
break;
//该语句会触发遍历器对象的return方法
}
for(let val of s){
console.log(val);
throw new Error(); //该语句会触发遍历器对象的throw方法
}
总结
Iterator遍历器是ES6最为重要和基础的概念之一,很多新的ES6的语法都在它的基础上实现。
概念性地说,遍历器是一种线性输出“集合”属性值的机制。对于原生的线性结构,如Array、Set、Map等,js引擎直接为我们部署了遍历器。但是对于非线性结构,如普通对象,一旦我们为其定义了遍历器,就相当于对它的属性进行了线性转化,因此它就可以像普通的线性结构一样按序输出属性值。
最后
以上就是复杂铃铛为你收集整理的ES6之遍历器Iterator一、什么是遍历器二、如何部署遍历器三、遍历器的应用总结的全部内容,希望文章能够帮你解决ES6之遍历器Iterator一、什么是遍历器二、如何部署遍历器三、遍历器的应用总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复