[ES6深度解析]2:迭代器(Iterators )和for of循环

旧版本for循环

如何循环遍历数组中的元素?20年前,当JavaScript被引入时,你会这样做:

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

从ES5开始,你就可以使用内置的forEach方法:

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

这个版本稍微短一些,但有一个小缺点:不能使用break语句跳出这个循环,也不能使用return语句从封闭函数返回。如果有一个简洁的for循环语法来循环数组元素,那就太好了。

for-in循环怎么样?

for (var index in myArray) {    //不要这么做
  console.log(myArray[index]);
}

这不是个好办法,原因如下:

  • 在这段代码中,分配给index的值是字符串“0”、“1”、“2”等等,而不是实际的数字。由于不想触发字符串算术(“2”+ 1 ==“21”),这方式是不方便的。
  • 循环体不仅会对数组元素执行,还会对其他人添加的任何其他expando属性执行。例如,如果你的数组有一个可枚举的属性myArray.name,那么这个循环将额外执行一次index == "name"。甚至可以访问数组原型链上的属性。
  • 最令人惊讶的是,在某些情况下,该代码可以以任意顺序循环遍历数组元素。
    简而言之,for-in被设计用于带有字符串键的普通旧对象。对于数组,就没那么好了。

强大的for-of循环

ES6承诺不会破坏你已经编写的JS代码。数以百万计的网站依赖于for-in的行为,甚至它在数组上的行为。因此,在数组中使用for-in时,修复for-in并没有什么问题。ES6改进问题的唯一方法是添加某种新的循环语法。这就是:

for (var value of myArray) {
  console.log(value);
}
  • 这是迄今为止最简洁、最直接的数组元素循环语法
  • 它避免了for-in的所有陷阱
  • forEach()不同,它使用breakcontinuereturn

for - in循环用于遍历对象属性。
for - of循环用于遍历数据,比如数组中的值。

其他集合也支持for-of

for - of不仅仅适用于数组。它也适用于大多数数组类对象,如DOM节点列表。它也适用于字符串,将字符串作为Unicode字符序列处理:

for (var chr of "😺😲") {
  alert(chr);
}

它也适用于MapSet对象。

Set对象有利于消除重复数据:

var uniqueWords = new Set(words);
for (var word of uniqueWords) {
  console.log(word);
}

Map略有不同:它内部的数据是由键值对组成的,所以你需要使用解构(destructuring)将键和值解压缩为两个独立的变量:

for (var [key, value] of phoneBookMap) {
  console.log(key + "'s phone number is: " + value);
}

你可以看到:JS已经有相当多的不同的集合类,甚至更多的在路上。for-of被设计为用于所有这些语句的主力循环语句。for - of不能用于普通的旧对象,但如果你想遍历一个对象的属性,你可以使用for - in(这就是它的用途)或内置的Object.keys():

// 输出一个对象所有可以枚举的属性值
for (var key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}

for - of 本质

Good artists copy, great artists steal.” —Pablo Picasso

ES6中一个众所周知的秘密是,添加到语言中的新特性并不是凭空而来的。

大多数已经在其他语言中试用过,并被证明是有用的。例如,for - of循环类似于c++Javac#Python中的循环语句。与它们一样,它可以使用该语言及其标准库提供的几种不同的数据结构。但它也是语言中的一个扩展点。就像其他语言中的for/foreach语句一样,for - of完全基于方法调用

数组,映射,集合,以及我们讨论过的其他对象的共同点是它们都有一个迭代器方法。还有另一种对象也可以有迭代器方法:任何你想要的对象

就像你可以将myObject.tostring()方法添加到任何对象中,然后JS突然知道如何将该对象转换为字符串一样,你可以将myObject[Symbol.iterator]()方法添加到任何对象中,然后JS突然知道如何循环该对象。

例如,假设您正在使用jQuery,尽管你非常喜欢.each(),但你也希望jQuery对象能够与for - of一起工作。以下是如何做到这一点:

// 因为jQuery对象是类似数组的,所以给它们提供与数组相同的迭代器方法
jQuery.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];

[Symbol.iterator]语法看起来很奇怪。它与方法名有关。标准委员会可以把这个循环方法命名为.iterator(),但是,现有的代码可能已经有一些带有.iterator()方法的对象,这可能会非常令人困惑。因此,标准使用Symbol而不是字符串.iterator()作为这个方法的名称。

Symbol在ES6中是新的特性。现在你只需要知道这个标准可以定义一个全新的符号,比如Symbol.iterator,保证不会与任何现有代码冲突。需要权衡的是语法有点奇怪。但对于这个多功能的新特性和出色的向后兼容性来说,这只是一个很小的代价。

具有[Symbol.iterator]()方法的对象称为iterable。可迭代对象的概念在整个语言中被使用,不仅在for-of中,而且在Map和Set构造函数、解构赋值和新的扩展操作符(spread operator)中。

迭代器对象

现在,我们可能永远不必从头开始实现自己的迭代器对象。但是为了完整起见,让我们看看迭代器对象是什么样子的。for-of循环首先调用集合上的[Symbol.iterator]()方法。返回一个新的迭代器对象。迭代器对象可以是任何具有.next()方法的对象;for-of循环将反复调用此方法,每次循环调用一次。例如,这可能是最简单的迭代器对象:

var zeroesForeverIterator = {
  [Symbol.iterator]: function () {
    return this;
  },
  next: function () {
    return {done: false, value: 0};
  }
};

每次调用这个.next()方法时,它都会返回相同的结果,告诉for-of循环:

  • 我们还没有完成迭代;
  • 下一个值是0。

这意味着 for (value of zeroesForeverIterator) {}将是一个无限循环。当然,典型的迭代器不会如此简单。

这种具有.done.value属性的迭代器设计从表面上看与其他语言中的迭代器工作方式不同。在Java中,迭代器有各自的. hasnext().next()方法。在Python中,它们有一个.next()方法,当没有更多值时抛出StopIteration。但这三种设计基本上都是返回相同的信息。

迭代器对象还可以实现可选的.return().throw(exc)方法。如果由于异常或breakreturn语句导致循环过早退出,则for-of循环调用.return()。如果迭代器需要做一些清理或释放它正在使用的资源,那么它可以实现.return()。大多数迭代器对象都不需要实现它,.throw(exc)更是一种特殊情况:for-of根本不会调用它。

现在我们有了所有的细节,我们可以使用一个简单的for-of循环,并根据底层的方法调用重写它。

首先是for-of循环:

for (VAR of ITERABLE) {
  STATEMENTS
}

下面是一个大致的等效的写法,使用了底层方法和一些临时变量:

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  VAR = $result.value;
  STATEMENTS
  $result = $iterator.next();
}
posted @ 2021-08-20 16:42  Max力出奇迹  阅读(87)  评论(0编辑  收藏  举报
返回顶部↑