探索ES6 Iterator(遍历器)

前言:

半年前快速过了一遍ES6语法,掌握并熟练了一些常用的ES6语法,比如:class、继承、模板字符串、解构赋值、导入导出、Promise等等,然而对于一些ES6的其他新特性,并没有认真研究,最近学习ES8时,学习了Object.entries方法,然后遇到了一个问题,这个问题是:为什么对象不可以用for of进行遍历?这才来研读了一下Iterator,很庆幸我在这里找到了答案,本文参考了阮一峰老师的ES6标准入门,为此买了一本纸质版的ES6标准入门以示感谢!

1、Iterator是什么?

我的理解就是为不同的数据结构统一遍历接口,以便按顺序进行遍历,通俗一点讲就是可以共用ES6的for of遍历机制

2、一起来看看ES6标准入门中关于Iterator的简单模拟实现
var it = makeIterator(['a', 'b']); 

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

function makeIterator(array) { //此方法会返回一个对象,改对象依赖于一个索引
  var nextIndex = 0; //该索引的作用类似于指针
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++], done: false} :
        {value: undefined, done: true};
    }
  };
}

首先调用makeIterator方法得到一个遍历器对象,该对象里面有一个next属性对应一个方法,该方法用于进行遍历,该方法共享闭包作用域里面的nextIndex指针来进行遍历,每次执行next方法都会将nextIndex加1,指向数组的下一个成员,当指针nextIndex大于数组长度时,将返回{value: undefined, done: true},即完成状态。此处抛一个问题:for of遍历数据结构时是依据什么跳出遍历过程的?

3、Iterator接口部署在哪里

并不是所有的数据结构都部署了Iterator接口,只有部署了Iterator的数据结构才可以使用for of进行遍历,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性

console.log(Object.prototype[Symbol.iterator])
//undefined
console.log(Array.prototype[Symbol.iterator])
//function values() { [native code] }
console.log(String.prototype[Symbol.iterator])
//function [Symbol.iterator]() { [native code] }

这说明对象是没有部署Iterator接口的,而数组和字符串是部署了遍历借口的,下面我们来验证一下:

const obj={
    "name":"luochao",
    "age" : "age"
}
for (const x of obj) { 
    console.log(x); //obj[Symbol.iterator] is not a function
}
for (const y of ["1","2","3"]) { 
    console.log(y);  //1,2,3
}
for (const z of "lc") { 
    console.log(z);  //l,c
}

当然你也可以这样遍历

let obj4= [6,7,8]
let xx=obj4[Symbol.iterator](); //拿到遍历器对象
for(let i=0;i<obj4.length;i++){
    console.log(xx.next())
}
//Object {value: 6, done: false}
//Object {value: 7, done: false}
//Object {value: 8, done: false}
console.log(xx.next())// {value: undefined,done: true}

此处抛出一个问题?你是否发现这两种遍历数组的方式差别在哪里了呢?你是否可以模拟的写出Array.prototype[Symbol.iterator]遍历接口的实现逻辑呢?聪明的你可能已经想出来了

原生部署了遍历接口的数据结构有:Array、Map、Set、String、TypedArray、函数的 arguments 对象

如果你没有写出来Array.prototype[Symbol.iterator]遍历接口的实现,也不要灰心,跟着我接着往下看

4、一个对象如果要具备可被for…of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可),不理解不要紧,看一下ES6标准人门中的代码
class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    }
    return {done: true, value: undefined};
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value); // 0, 1, 2
}

这里我们看下使用typescript编译后的代码

var RangeIterator = (function () {
    
    
    function RangeIterator(start, stop) {
    
    
        this.value = start;
        this.stop = stop;
    }
    RangeIterator.prototype[Symbol.iterator] = function () {
    
     return this; };
    RangeIterator.prototype.next = function () {
    
    
        var value = this.value;
        if (value < this.stop) {
            this.value++;
            return { done: false, value: value };
        }
        return { done: true, value: undefined };
    };
    return RangeIterator;
}());

通过编译后的代码可以发现遍历器接口被加在了RangeIterator.prototype[Symbol.iterator]上,发现了什么?for of遍历range(0, 3)生成的对象,直接拿到值,是不是可以看成range(0,3)生成的对象直接调用了原型RangeIterator.prototype上的[Symbol.iterator]方法拿到了遍历器对象,然后再调用了原型上的next方法拿到{ done: false, value: value }对象,然后在取对象的value属性拿到实际的value值:0,1,2,for of是不是真的做了这些事呢?

5、我们来重写一下Array.prototype[Symbol.iterator]函数(这只是我的假设,有不对的地方欢迎指正)
Array.prototype[Symbol.iterator]=function(){
    
    
    this.nextIndex=0;
    this.next=function(){
    
    
        return this.nextIndex < this.length ?
        {value: this[this.nextIndex++], done: false} :
        {value: 1, done: true};
    }
    return this;
}
let arr3=[9,8,7];
for(let attr of arr3){
  console.log(attr) //9,8,7
}
let xx=arr3[Symbol.iterator](); //拿到遍历器对象
for(let i=0;i<arr3.length;i++){
    console.log(xx.next())
}
//Object {value: 9, done: false}
//Object {value: 8, done: false}
//Object {value: 7, done: false}
console.log(xx.next())
//Object {value: undefined, done: true}

应该是眼前一亮吧?我的设想是for of遍历具有iterator接口的数据结构,会先执行[Symbol.iterator]方法,然后返回遍历器对象(这里以arr3为例就是返回arr3),然后调用遍历器对象(arr3)的next方法拿到Object {value: 9, done: false},然后调用返回对象的value属性拿到具体的值

6、我们再来看一段代码验证我们的构想,并回答开始的一个问题:for of遍历数据结构时是依据什么跳出遍历过程的?
function Obj(value) {
    
    
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
    
    
  var iterator = { next: next };

  var current = this;

  function next() {
    
    
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    } else {
      return { done: true };
    }
  }
  return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;
for (var i of one){
  console.log(i); // 1, 2, 3
}

同样的我们来分析这段代码,for of循环执行,调用one对象(其实引用js高程上的话说应该叫指针)的[Symbol.iterator]方法拿到遍历器对象iterator,然后调用遍历器对象iterator的next方法,第一次执行current等于one指针指向的对象,所以会返回{ done: false, value: 1 },并且执行 current = current.next 后this就变成了two指针指向的对象,然后接下来遍历就应该遍历two指针指向的对象了,什么时候跳出,相信你也看出来了当current取three的next属性时,current为undefined,则返回{ done: true },当for of拿到{ done: true }将会跳出遍历,为了验证我的猜想,我把代码做了如下修改

function Obj(value) {
    
    
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
    
    
  var iterator = { next: next };

  var current = this;

  function next() {
    
    
    if (current) {
      var value = current.value;
      current = current.next;
      return { done: false, value: value };
    } else {
      return { done: false,value:"无限遍历"};
    }
  }
  return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;
for (var i of one){
  console.log(i); // 1, 2, 3
}

当我把{done:true}修改为{ done: false,value:”无限遍历”}时,你讲会看到这个for of循环根本停不下来了,接下来控制台就是满屏的”无限循环”字眼,所以for of跳出循环是依据{done:true}进行跳出的

7、回到最开始的问题,为什么对象不可以用for of进行遍历?

最直接的回到应该是对象没有部署[Symbol.iterator]遍历接口,那它为什么不部署遍历接口呢?再次回到定义:部署遍历接口是为了数据结构的成员能够按某种次序排列,当你看了Array.prototype[Symbol.iterator]的实现

Array.prototype[Symbol.iterator]=function(){
    
    
    this.nextIndex=0;
    this.next=function(){
    
    
        return this.nextIndex < this.length ?
        {value: this[this.nextIndex++], done: false} :
        {value: 1, done: true};
    }
    return this;
}

你可能会注意到了this.nextIndex,value:this[this.nextIndex++],这充分表明部署遍历接口的数据接口都是有线性规律的,而对象的键值都是任意的,杂乱无章的,这就失去了部署遍历接口的意义

8、如何查看数据结构的遍历接口

数据结构之所以可以用for of进行遍历,是因为数据结构部署了遍历接口,而用for of进行循环遍历时,会默认调用待遍历数据的遍历接口,遍历接口一般是数据结构原型的某一个方法(比如:values,entries方法),下面我来教你如何查看数据结构的遍历接口:查看对象原型上的 Symbol.iterator 属性对应的方法

const set = new Set([
  ['lc', 22],
  ['yx', 21]
]);
console.log(set);


set结构的 Symbol.iterator 属性对应的方法是values方法,这说明我们在调用for of对set结构进行遍历的时候,他相当于会调用values()方法拿到遍历器对象(其实 set[Symbol.iterator]=== set.values),也就是说你直接用for of遍历 set.values() 方法或者 set 是没有区别的

for (const v of set){
  console.log(v); //["lc", 1],['yx', 21]
}
for (let v of set.values()){
  console.log(v)//["lc", 1],['yx', 21]
}
console.log(set[Symbol.iterator]===set.values) //true
9、我有话说
let arr3=[9,8,7];
let xx=arr3[Symbol.iterator](); //拿到遍历器对象
console.log(xx) //[9, 8, 7, nextIndex: 0]

let arr4=[9,8,7];
let xxx=arr4[Symbol.iterator];
console.log(xxx()) //为什么这里就不能拿到上面带nextIndex的数组了呢

欢迎大佬留言指正,万分感谢!!!

猜你喜欢

转载自blog.csdn.net/luo1055120207/article/details/75453247