重学ES6(七)Iterator 遍历器

180 阅读3分钟

基础概念

ES6 表示集合的数据结构有以下四种,并可以组合使用他们,定义自己的数据结构。

  • 数组 (Array)
  • 对象 (Object)
  • Map
  • Set

Iterator 是一种机制。它也是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据只要部署 Iterator 接口,就可以完成遍历操作。

Iterator有三个作用

  • 为各种数据结构,提供了一个统一的、简便的访问接口
  • 使得数据结构的成员能够按某种次序排列
  • ES6创造了 for...of 循环

Iterator 遍历的详细过程是

1、创建一个指针对象,指向当前数据结构的起始位置
2、第一次调用指针对象的 next 方法,可以将指针指向数据结构的第一个成员
3、第二次调用指针对象的 next 方法,指针就指向数据结构的第二个成员
4、不断调用 next 方法,直到它指向数据结构的结束位置

下面模拟的 next 的例子

var it = makeIterator(['a', 'b'])

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

function makeIterator(array) {
	var nextIndex = 0
    return {
    	next: function() {
        	return nextIndex < array.length ?
              {value: array[nextIndex++], done: false} :
              {value: undefined, done: true}
        }
    }
}

默认 Iterator 接口

一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是可遍历的(Iterable)。

大家应该还记得我们在 Symbol 那一章说过的好多属性把,这里 Iterator 接口部署在数据结构的 Symbol.iterator 属性。

eg1:

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
}

如果我们想要给对象能够使用 for...of,原生的 obj 是不能 iterable 的,我们这里可以使用遍历器给他添加,它就可以使用for of 方法了。

var obj = {a: 1, b: 2, c: 3}

for (var item of obj) {
    console.log(item)
}
// TypeError: obj is not iterable
-------------------------------------

var obj = {
    a: 1,
    b: 2,
    [Symbol.iterator]: function() {
        var iterator = {next: next}
        var current = 0    

        function next() {
            if (current < 3) {
                return {done: false, value: current++}
            } else {
                return {done: true}
            }
        }

        return iterator
    }
}

for (var item of obj) {
    console.log(item)
}
// 0 1 2

原生具备 Iterator 接口的数据结构如下

- Array
- Map
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象

下面是一个数组的 Symbol.iterator

let arr = [1, 2, 3]

let iter = arr[Symbol.iterator]()

console.log(JSON.stringify(iter.next()))
// {"value":1,"done":false}
console.log(JSON.stringify(iter.next()))
// {"value":2,"done":false}
console.log(JSON.stringify(iter.next()))
// {"value":3,"done":false}
console.log(JSON.stringify(iter.next()))
// {"done":true}

对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of 循环会自动遍历他们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在 Symbol.iterator 属性上部署,才能被 for...of 循环遍历。

// 类具有 Symbol.iterator
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

上面代码是一个类部署有 Iterator 接口,所以 new 出来的这个类的实例化对象是具备 Symbol.iterator 属性的。

下面是通过遍历器实现指针结构的例子

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

这里我们为对象添加 Iterator 接口的例子

let obj = {
  data: [ 'hello', 'world' ],

  [Symbol.iterator]() {
    const self = this;
    let index = 0;
  
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    }
  
  }
}

for (var a of obj) {
    console.log(a)
}
// hello
// world

对于类似数组得对象(存在数值键名和 length 属性),部署 Iterator 接口,有一个简单的方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator]

// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

[...document.querySelectorAll('div')]

NodeList 对象是类似数组的对象,本身就具有遍历接口的,可以直接遍历。上面代码将它本身的遍历接口改成数组的 Symbol.iterator 属性,看不到任何影响。

下面例子是再类似数组的对象调用数组的 Symbol.iterator 方法的例子

let iterator = {
	0: 'a',
    1: 'b',
    2: 'c',
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator]
}

for (let a of iterator) {
    console.log(a)
}
// a b c

注意这里对象使用数组的遍历器,是因为是类似数组的对象,key 都是数字,如果是其他的普通对象部署数组的遍历器是没用效果的。

调用 Iterator 接口的场合

(1) 解构赋值,对数组和 Set 解构进行解构赋值时,会默认调用 Symbol.iterator 方法

let set = new Set()
set.add('a').add('b').add('c')

let [x, y] = set
// x => a
// y => b

let [first, ...rest] = set
// first => a
// rest => ['a', 'b']

(2) 扩展运算符(...)也会调用 Iterator 接口

let str = 'hello'
[...str]	// ['h', 'e', 'l', 'l', 'o']

(3) yield* yield* 后面跟的是一个可遍历解构,它会调用遍历器

字符串的 Iterator 接口

var someString = 'hi'
typeof someString[Symbol.iterator]
// 'function

var it = someString[Symbol.iterator]()

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

上面代码里面,调用了 Symbol.iterator 返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对字符串的遍历。

可以覆盖原生的 Symbol.iterator 方法,达到修改遍历器行为的目的。

var str = new String("hi");

[...str] // ["h", "i"]

str[Symbol.iterator] = function() {
  return {
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
}

console.log([...str])
console.log(str)

上面代码中,字符串 str的 Symbol.iterator方法被修改了

Iterator 接口与 Generator 函数

Symbol.iterator 方法最简单的实现,可以使用 Generator,这里大概了解下,下一章我们详细学习 Generator。

let myIterable = {
	[Symbol.iterator]: function() {
    	yield 1;
        yield 2;
        yield 3;
    }
}
[...myIterable]		// 1 2 3

// 或者
let obj = {
	* [Symbol.iterator] () {
    	yield 'hello';
        yield 'world';
    }
}

for (let x of obj) {
	console.log(x)
}
// 'hello'	
// 'world'

遍历对象的 return 和 throw

遍历器对象除了具有 next 方法,还可以有 return 和 throw 方法。如果是你自己写遍历器对象生成函数,那么 next 方法是必须部署的,return 和 throw 是否部署是可选的。

return 方法使用场合是 for...of 循环提前退出,通常是因为出错或者有break 语句,就会调用 return 方法。如果一个对象在完成遍历之前,需要清理或释放资源,就可以部署 return 方法。

function readLinesSync (file) {
    return {
        [Symbol.iterator]() {
            return {
                next() {
                    return {done: false}
                },
                return() {
                    file.close()
                    return {done: true}
                }
            }
        }
    }
}

下面这两种情况都会触发执行 return 方法

// 情况一
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 情况二
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

上面代码中,情况一输出文件第一行后,就会执行 return 方法,关闭文件。 情况二会执行 return 方法关闭文件后,在抛出错误。

throw 方法主要配合 Generator 函数使用。