ES6之遍历器Iterator

一、什么是遍历器

在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可以知道是否应该退出循环。

扫描二维码关注公众号,回复: 8725091 查看本文章

我们调用上述函数时传入的是数组,这可能让人误以为这个函数只能生成适配数组的遍历器,实际上并不是这样的。比如下面的结构:

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属性本身)。这也就意味着,以下的结构都可以适用这个遍历器:

  1. Array
  2. Map
  3. Set
  4. String
  5. TypedArray
  6. 函数的 arguments 对象
  7. 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引擎直接为我们部署了遍历器。但是对于非线性结构,如普通对象,一旦我们为其定义了遍历器,就相当于对它的属性进行了线性转化,因此它就可以像普通的线性结构一样按序输出属性值。

发布了37 篇原创文章 · 获赞 90 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_41694291/article/details/103432571