前端笔记

Iterator 迭代器

2017-10-03  本文已影响4人  faremax

由于 ES6 中引入了许多数据结构, 算上原有的包括Object, Array, TypedArray, DataView, buffer, Map, WeakMap, Set, WeakSet等等, 数组需要一个东西来管理他们, 这就是遍历器(iterator)。

for...of

遍历器调用通常使用 for...of 循环, for...of 可以遍历具有 iterator 的对象, ES6中默认只有数组, Set, Map, String, Generator和一些类数组对象(arguments, DOM NodeList)带有遍历器, 其他的数据结构需要自己定义遍历器。

var arr = ["red", "green", "blue"];
for(let v of arr){    //相当于 for(let i in arr.values())
  console.log(v);     //依次输出 "red", "green", "blue"
}
for(let i in arr){
  console.log(i);     //依次输出 0, 1, 2
}
for(let [key, value] of arr.entries()){
  console.log(key + ": " + value);      //依次输出 0: "red", 1: "green", 2: blue"
}
for(let key of arr.keys()){
  console.log(key);     //依次输出 0, 1, 2
}

不难看出 for...of 默认得到值, 而 for...in 只能得到索引。当然数组的 for...of 只返回数字索引的属性, 而 for...in 没有限制:

var arr = ["red", "green", "blue"];
arr.name = "color";
for(let v of arr){
  console.log(v);     //依次输出 "red", "green", "blue"
}
for(let i in arr){
  console.log(arr[i]);     //依次输出 "red", "green", "blue", "color"
}
var set = new Set(["red", "green", "blue"]);
for(let v of set){    //相当于 for(let i in arr.values())
  console.log(v);     //依次输出 "red", "green", "blue"
}
for(let [key, value] of set.entries()){
  console.log(key + ": " + value);      //依次输出 "red: red", "green: green", "blue: blue"
}
for(let key of set.keys()){
  console.log(key);     //依次输出 "red", "green", "blue"
}
var map = new Map();
map.set("red", "#ff0000");
map.set("green", "#00ff00");
map.set("blue", "#0000ff");
for(let [key, value] of map){    //相当于 for(let i in arr.entries())
  console.log(key + ": " + value);      //依次输出 "red: #ff0000", "green: #00ff00", "blue: #0000ff"
}
for(let value of map.values()){
  console.log(value);     //次输出 "#ff0000", "#00ff00", "#0000ff"
}
for(let key of map.keys()){
  console.log(key);     //次输出 "red", "green", "blue"
}
var str = "Hello";
for(let v of str){
  console.log(v);     //依次输出 "H", "e", "l", "l", "o"
}
// DOM NodeList
var lis = document.getElementById("li");
for(let li of lis){
  console.log(li.innerHTML);   //遍历每个节点
}

//arguments
function fun(){
  for(let arg of arguments){
    console.log(arg);          //遍历每个参数
  }
}

不是所有类数组对象都有 iterator, 如果没有, 可以先用Array.from()进行转换:

var o = {0: "red", 1: "green", 2: "blue", length: 3};
var o_arr = Array.from(o);
for(let v of o_arr){
  console.log(v);        //依次输出 "red", "green", "blue"
}

技巧1: 添加以下代码, 使 for...of 可以遍历 jquery 对象:

$.fn[Symbol.iterator] = [][Symbol.iterator];

技巧2: 利用 Generator 重新包装对象:

function* entries(obj){
  for(let key of Object.keys(obj)){
    yield [key, obj[key]];
  }
}
var obj = {
  red: "#ff0000",
  green: "#00ff00",
  blue: "#0000ff"
};
for(let [key, value] of entries(obj)){
  console.log(`${key}: ${value}`);        //依次输出 "red: #ff0000", "green: #00ff00", "blue: #0000ff"
}

几种遍历方法的比较

iterator 与 [Symbol.iterator]

iterator 遍历过程是这样的:

  1. 创建一个指针对象, 指向当前数据结构的起始位置。即遍历器的本质就是一个指针。
  2. 调用一次指针的 next 方法, 指针指向第一数据成员。之后每次调用 next 方法都会将之后向后移动一个数据。
  3. 知道遍历结束。

我们实现一个数组的遍历器试试:

var arr = [1, 3, 6, 5, 2];
var it = makeIterator(arr);
console.log(it.next());       //Object {value: 1, done: false}
console.log(it.next());       //Object {value: 3, done: false}
console.log(it.next());       //Object {value: 6, done: false}
console.log(it.next());       //Object {value: 5, done: false}
console.log(it.next());       //Object {value: 2, done: false}
console.log(it.next());       //Object {value: undefined, done: true}

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

由这个例子我们可以看出以下几点:

其实一个 id 生成器就很类似一个遍历器:

function idGen(){
  var id = 0;
  return {
    next: function(){ return id++; }
  };
}
var id = idGen();
console.log(id.next());   //0
console.log(id.next());   //1
console.log(id.next());   //2
//...

对于大多数数据结构, 我们不需要再像这样写遍历器函数了。因为他们已经有遍历器函数[Symbol.iterator], 比如Array.prototype[Symbol.iterator] 是数组结构的默认遍历器。

下面定义一个不完整(仅包含add()方法)的链表结构的实例:

function Node(value){
  this.value = value;
  this.next = null;
}
function LinkedList(LLName){
  this.head = new Node(LLName);
  this.tail = this.head;
}
var proto = {
  add: function(value){
    var newNode = new Node(value);
    this.tail = this.tail.next = newNode;
    return this;
  }
}
LinkedList.prototype = proto;
LinkedList.prototype.constructor = LinkedList;
LinkedList.prototype[Symbol.iterator] = function(){
  var cur = this.head;
  var curValue;
  return {
    next: function(){
      if(cur !== null){
        curValue = cur.value;
        cur = cur.next;
        return {value: curValue, done: false}
      } else {
        return {value: undefined, done: true}
      }
    }
  };
}

var ll = new LinkedList("prime");
ll.add(1).add(2).add(3).add(5).add(7).add(11);
for(let val of ll){
  console.log(val);    //依次输出 1, 2, 3, 5, 7, 11
}

注意, 如果遍历器函数[Symbol.iterator]返回的不是如上例所示结构的对象, 会报错。
当然, 如果不不喜欢用for...of(应该鲜有这样的人吧), 可以用 while 遍历:

var arr = [1, 2, 3, 5, 7];
var it = arr[Symbol.iterator];
var cur = it.next();
while(!cur.done){
  console.log(cur.value);
  cur = it.next();
}

以下操作会在内部调用相应的 iterator:

Generator 与遍历器

iterator 使用 Generator 实现会更简单:

var it = {};
it[Symbol.iterator] = function* (){
  var a = 1, b = 1;
  var n = 10;
  while(n){
    yield a;
    [a, b] = [b, a + b];
    n--;
  }
}
console.log([...it]); //1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

当然, 以上代码还可以这样写:

var it = {
  *[Symbol.iterator](){
    var a = 1, b = 1;
    var n = 10;
    while(n){
      yield a;
      [a, b] = [b, a + b];
      n--;
    }
  }
}
console.log([...it]); //[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

遍历器对象的其他方法

以上的遍历器对象只提到了 next() 方法, 其实遍历器还有 throw() 方法和 return() 方法:

function readlineSync(file){
  return {
    next(){
      if(file.isAtEndOfFile()){
        file.close();
        return {done: true};
      }
    },
    return(){
      file.close();
      return {done: true};
    }
  }
}

上面实现了一个读取文件内数据的函数, 当读取到文件结尾跳出循环, 但是当循环跳出后, 需要做一些事情(关闭文件), 以防内存泄露。这个和 C++ 中的析构函数十分类似, 后者是在对象删除后做一些释放内存的工作, 防止内存泄露。

上一篇下一篇

猜你喜欢

热点阅读