ES6 迭代器(Iterator)和 for.of循环使用方法学习(总结)

一、什么是迭代器?

生成器概念在Java,Python等语言中都是具备的,ES6也添加到了JavaScript中。Iterator可以使我们不需要初始化集合,以及索引的变量,而是使用迭代器对象的 next 方法,返回集合的下一项的值,偏向程序化。

迭代器是带有特殊接口的对象。含有一个next()方法,调用返回一个包含两个属性的对象,分别是value和done,value表示当前位置的值,done表示是否迭代完,当为true的时候,调用next就无效了。

ES5中遍历集合通常都是 for循环,数组还有 forEach 方法,对象就是 for-in,ES6 中又添加了 Map 和 Set,而迭代器可以统一处理所有集合数据的方法。迭代器是一个接口,只要你这个数据结构暴露了一个iterator的接口,那就可以完成迭代。ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。

二、如何使用迭代器?

1、默认 Iterator 接口

数据结构只要部署了 Iterator 接口,我们就成这种数据结构为“可遍历”(Iterable)。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,或者说,一个数据结构只要具有 Symbol.iterator 数据,就可以认为是“可遍历的”(iterable)。

可以供 for…of 消费的原生数据结构

    Array
    Map
    Set
    String
    TypedArray(一种通用的固定长度缓冲区类型,允许读取缓冲区中的二进制数据)
    函数中的 arguments 对象
    NodeList 对象

可以看上面的原生数据结构中并没有对象(Object),为什么呢?

那是因为对象属性的遍历先后顺序是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口就等于部署一种线性变换。

做如下处理,可以使对象供 for…of 消费:

// code1
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

2、调用 Iterator 接口的场合

(1) 解构赋值

// code2
let set = new Set().add(\'a\').add(\'b\').add(\'c\');
let [x,y] = set;
// x=\'a\'; y=\'b\'
let [first, ...rest] = set;
// first=\'a\'; rest=[\'b\',\'c\'];

(2) 扩展运算符

// code3
// 例一
var str = \'hello\';
[...str] // [\'h\',\'e\',\'l\',\'l\',\'o\']
// 例二
let arr = [\'b\', \'c\'];
[\'a\', ...arr, \'d\']
// [\'a\', \'b\', \'c\', \'d\']

(3)Generator 函数中的 yield* 表达式(下一章介绍)

// code4
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }

(4)其它场合

    for..of
    Array.from
    Map()、Set()、WeakMap()、WeakSet()
    Promise.all()
    Promise.race()

3、for…of 循环的优势

先看看,数组 forEach 方法的缺点:

// code5
myArray.forEach(function (value) {
 console.log(value);
});

这个写法的问题在于,无法中途跳出 forEach 循环,break 命令或 return 命令都不能生效。

再看看,对象 for…in 的循环的缺点:

for (var index in myArray) {
 console.log(myArray[index]);
};
    数组的键名是数字,但是 for…in 循环是以字符串作为键名,“0”、“1”、“2”等。
    for…in 循环不仅可以遍历数字键名,还会遍历手动添加的期推荐,甚至包括原型链上的键。
    某些情况下,for…in 循环会议任意顺序遍历键名
    for…in 遍历主要是为遍历对象而设计的,不适用于遍历数组

那么,for…of 有哪些显著的优点呢?

    有着同 for…in 一样的简洁语法,但是没有 for…in 那些缺点
    不同于 forEach 方法,它可以与 break、continue 和 return 配合使用
    提供了遍历所有数据结构的统一操作接口
for (var n of fibonacci) {
 if (n > 1000) {
  break;
  console.log(n);
 }
}

4、各数据类型如何使用 for…of 循环?

(1)数组

for…of 循环允许遍历数组获得键值

var arr = [\'a\', \'b\', \'c\', \'d\'];
for (let a in arr) {
  console.log(a); // 0 1 2 3
}
for (let a of arr) {
  console.log(a); // a b c d
}

for…of 循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的值

let arr = [3, 5, 7];
arr.foo = \'hello\';
for (let i in arr) {
  console.log(i); // \"0\", \"1\", \"2\", \"foo\"
}
for (let i of arr) {
  console.log(i); // \"3\", \"5\", \"7\"
}

(2)Map 和 Set 结构

var engines = new Set([\"Gecko\", \"Trident\", \"Webkit\", \"Webkit\"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set(\"edition\", 6);
es6.set(\"committee\", \"TC39\");
es6.set(\"standard\", \"ECMA-262\");
for (var [name, value] of es6) {
  console.log(name + \": \" + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262

由上述的代码可以看出,for…of 循环遍历Map 和 Set 结构时,遍历的顺序是按照各个成员被添加进数据结构的顺序,Set 结构遍历时返回的是一个值,而 Map 结构遍历时返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

(3)类数组对象

字符串

// 普通的字符串遍历
let str = \"yuan\";
for (let s of str) {
 console.log(s); // y u a n
}

// 遍历含有 32位 utf-16字符的字符串
for (let x of \'a\\uD83D\\uDC0A\') {
 console.log(x);
}
// \'a\'
// \'\\uD83D\\uDC0A\'

DOM NodeList 对象

let paras = document.querySelectorAll(\"p\");
for (let p of paras) {
 p.classList.add(\"test\");
}

arguments 对象

function printArgs() {
 for (let x of arguments) {
  console.log(x);
 }
}
printArgs(\"a\", \"n\");
// \"a\"
// \"n\"

没有 Iterator 接口类数组对象的遍历处理

借用 Array.from 方法处理

let arrayLike = {
  length: 2,
  0 : \'a\',
  1 : \'b\'
};
// 报错
for (let x of arrayLike) {
  console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
  console.log(x);
}

(4)对象

对于普通对象,不能直接使用 for…of 遍历,否则会报错,必须部署了 Iterator 接口才能使用。如下两种方法部署:

// 方法一:使用 Object.keys 方法讲对象的键名生成一个数组
for (var key of Object.keys(someObject)) {
 console.log(key + \": \" + someObject[key]);
}

// 方法二:使用Generator 函数将对象重新包装一下
function * entries(obj) {
  for (let key of Object.keys(obj)) {
    yield[key, obj[key]];
  }
}
for (let[key, value] of entries(obj)) {
  console.log(key, \"->\", value);
}
// a -> 1
// b -> 2
// c -> 3

三、迭代器应用实例

1、斐波那契数列

下面我们就使用迭代器来自定义自己的一个斐波那契数列组,我们直到斐波那契数列有两个运行前提,第一个前提是初始化的前两个数字为0,1,第二个前提是将来的每一个值都是前两个值的和。这样我们的目标就是每次都迭代输出一个新的值。

var it = { [Symbol.iterator]() {
    return this
  },
  n1: 0,
  n2: 1,
  next() {
    let temp1 = this.n1,
    temp2 = this.n2;
    [this.n1, this.n2] = [temp2, temp1 + temp2]
    return {
      value: temp1,
      done: false
    }
  }
}

for (var i = 0; i < 20; i++) {
  console.log(it.next())
}

// 
  \"value\": 0,
  \"done\": false
} {
  \"value\": 1,
  \"done\": false
} {
  \"value\": 1,
  \"done\": false
} {
  \"value\": 2,
  \"done\": false
} {
  \"value\": 3,
  \"done\": false
} {
  \"value\": 5,
  \"done\": false
}... {
  \"value\": 2584,
  \"done\": false
} {
  \"value\": 4181,
  \"done\": false
}

2、任务队列迭代器

我们可以定义一个任务队列,该队列初始化时为空,我们将待处理的任务传递后,传入数据进行处理。这样第一次传递的数据只会被任务1处理,第二次传递的只会被任务2处理… 代码如下:

var Task = {
  actions: [],
  [Symbol.iterator]() {
    var steps = this.actions.slice();
    return { [Symbol.iterator]() {
        return this;
      },
      next(...args) {
        if (steps.length > 0) {
          let res = steps.shift()(...args);
          return {
            value: res,
            done: false
          }
        } else {
          return {
            done: true
          }
        }
      }
    }
  }
}

Task.actions.push(function task1(...args) {
  console.log(\"任务一:相乘\") return args.reduce(function(x, y) {
    return x * y
  })
},
function task2(...args) {
  console.log(\"任务二:相加\") return args.reduce(function(x, y) {
    return x + y
  }) * 2
},
function task3(...args) {
  console.log(\"任务三:相减\") return args.reduce(function(x, y) {
    return x - y
  })
});

var it = Task[Symbol.iterator]();
console.log(it.next(10, 100, 2));
console.log(it.next(20, 50, 100)) console.log(it.next(10, 2, 1))
 // 
任务一:相乘 {
  \"value\": 2000,
  \"done\": false
}任务二:相加 {
  \"value\": 340,
  \"done\": false
}任务三:相减 {
  \"value\": 7,
  \"done\": false
}

3、延迟执行

假设我们有一个数据表,我们想按大小顺序依次的获取数据,但是我们又不想提前给他排序,有可能我们根本就不去使用它,所以我们可以在第一次使用的时候再排序,做到延迟执行代码:

var table = {
  \"d\": 1,
  \"b\": 4,
  \"c\": 12,
  \"a\": 12
}
table[Symbol.iterator] = function() {
  var _this = this;
  var keys = null;
  var index = 0;

  return {
    next: function() {
      if (keys === null) {
        keys = Object.keys(_this).sort();
      }

      return {
        value: keys[index],
        done: index++>keys.length
      };
    }
  }
}

for (var a of table) {
  console.log(a)
} 
// a b c d

四、结语

本章内容,重点是明白 Iterator 接口的机制,以及 for…of 循环的使用方法。下一章介绍生成器函数 Generator 函数。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容