65.9K
CodeProject 正在变化。 阅读更多。
Home

理解 JavaScript 中循环的缺点和迭代协议

starIconstarIconstarIconstarIconstarIcon

5.00/5 (4投票s)

2020年8月22日

CPOL

5分钟阅读

viewsIcon

7094

学习不同循环结构的缺点,以及如何使用不同的迭代协议

引言

如果你已经使用JavaScript或任何语言编程一段时间了,那么for循环应该对你来说并不陌生。难道你没有注意到,许多编程语言(包括JavaScript)已经从使用for循环迭代数据转向使用迭代器对象,这些对象返回给定集合的下一个项。此外,迭代器让我们的生活在处理集合时变得更轻松、更高效。因此,理解JavaScript迭代器对每个开发者都很有益,本文将探讨这个主题。好的,那么我们开始吧。

背景

在开始之前,建议阅读JavaScript的Symbols,你可以在我的文章这里查看:JavaScript Symbols基础。其中一个原因是,我们可能会遇到Symbol.iterator的用法。需要注意的是,JavaScript迭代器不会取代for循环和/或其他循环结构。然而,迭代器的主要目标是消除循环的复杂性和易出错性。

目录

各种循环结构的缺点

For-loop

For循环是学习任何编程语言(包括JavaScript)的标志性内容之一。通常,在处理数组时,你会使用for循环来迭代数据元素。

让我们看下面的例子

/*simple loop*/
let dogsName = ['Tank', 'Diesel', 'Cooper', 'Bruno', 'Thor'];

for (let index = 0; index < dogsName.length; index++) {
  console.log(dogsName[index]);
}
/*end of simple loop*/

我知道,代码示例非常直接。创建一个复杂的例子怎么样?

/*complex loop*/
let x = 100;

for (let i = 0; i <= x; i++) {
  for (let j = 1; (j + x / 2) < x; j++) {
    for (let k = 1; k <= x; k++) {
      console.log("hello");
    }
  }
}
/*end of complex loop*/

当嵌套循环时,复杂度会增加,这可能导致在循环内部跟踪多个变量。因此,这会让你的循环容易出错。

Array.forEach

然而,如果你只是想迭代数据项,为什么还要使用for循环呢,对吧?Array.forEach来帮忙,但并非完全如此。为什么?在我们回答这个问题之前,先看一个例子。

/*forEach loop*/
let amounts = [1.25, 2.25, 3.25, 4.25];

/**
 * Outputs: 
 * 1.25
 * 2.25
 * 3.25
 * 4.25
 */
amounts.forEach((item) => {
    console.log(item);
});
/*end of forEach loop*/

现在,回到问题。因为使用Array.forEach循环不能让你在循环结构内使用break

让我们看下面的例子

/*forEach loop*/
let amounts = [1.25, 2.25, 3.25, 4.25];
amounts.forEach((item) => {

  if (item === 3.25) {
    break; //throws an error: "Uncaught SyntaxError: Illegal break statement"
  }
}); 
/*end of forEach loop*/ 

For-in loop

当你只对获取对象的属性感兴趣时,该怎么办?for-in循环应运而生。

让我们看下面的例子。

/** Start of for-in loop */
let customer = { firstName: "Jin Vincent", lastName: "Necesario", country: "Philippines" };

/**
 * Output:
 * firstName
 * lastName
 * country
 */
for (let key in customer) {
    console.log(key);
}

/** End of for-in loop */

然而,for-in循环只能迭代可枚举的、非Symbol属性。因此,迭代一个索引顺序很重要的Array是没有意义的。

让我们看下面的例子。

/** Start of for-in loop that iterates over an Array */
/** Warning: Please don't do this on your project/product! */

Array.prototype.bar = 1;

let products = [1001, "mouse", "monitor", { firstName: "Jin Vincent" }];

/**Output:
 * 0
 * 1
 * 2
 * 3
 * bar
 */

for (let prop in products) {
    console.log(prop); //this outputs the index of the array and bar
}
/** End of for-in loop that iterates over an Array*/

For-of loop

如果你想迭代一个数组或集合,并且不介意索引,for-of循环可以解决问题。

你可以将for-of循环与实现可迭代协议的对象或集合一起使用。

本文后面将详细介绍可迭代协议。

让我们看下面的例子。

/** Start of for of loop */
const xmenGoldTeam = ['Storm', 'Bishop', 'Colossus', 'Jean Grey', 'Iceman', 'Archangel'];

/**Outputs: 
 * Storm
 * Bishop
 * Colossus
 * Jean Grey
 * Iceman
 * Archangel
 */
for (let xmen of xmenGoldTeam) {
    console.log(xmen);
}
/** end of for of loop */

当一个对象不可迭代时,你就不能使用for-of循环来迭代其属性。

让我们看下面的例子

/** Start of for-of loop */
let customers = { firstName: "Jin Vincent", lastName: "Necesario", country: "Philippines" };

for (const customer of customers) {
    //TypeError: customers is not iterable
}
/** End of for-of loop */  

迭代协议

基本上,迭代协议是一组规则,对象在实现接口时需要遵循这些规则。换句话说,当使用某种协议(可以是可迭代协议或迭代器协议)时,你的对象应该实现以下功能和一些规则。

迭代协议的类型

  • 迭代器协议
  • 可迭代协议

什么是迭代器协议?

实现迭代器协议,你应该遵循以下约定

  • 对象应该实现.next()方法。
  • .next()方法必须返回一个包含valuedone属性的对象。

简而言之,当一个对象提供一个next()方法,该方法返回一组项的序列中的下一项,并且该项具有两个属性时,该对象就是一个迭代器。

这两个属性是done和value。

  • done - 一个布尔类型,指示是否有任何现有元素可以被迭代。
  • value - 当前元素。

让我们看下面的例子并遵循约定。

/*Start of an iterator protocol*/

const countries = {
    collection: ["Philippines", "Singapore", "Malaysia", "Canada", "Brazil", "Australia"],
    index: 0,
    next: function () {
        if (this.index < this.collection.length) {
            return {
                value: this.collection[this.index++],
                done: false
            }
        } else {
            return {
                done:true
            }
        }
    }
}

console.log(countries.next()); //output: {value: "Philippines", done: false}
console.log(countries.next()); //output: {value: "Singapore", done: false}
console.log(countries.next()); //output: {value: "Malaysia", done: false}
console.log(countries.next()); //output: {value: "Canada", done: false}
console.log(countries.next()); //output: {value: "Brazil", done: false}
console.log(countries.next()); //output: {value: "Australia", done: false}
console.log(countries.next()); //output: {done: true}

/*End of an iterator protocol*/

什么是可迭代协议?

在开始之前,有一点需要指出,JavaScript有众所周知的符号,通常用@@前缀表示。就像Symbol.iterator一样,这个符号被称为@@iterator

现在回到可迭代协议。当一个对象实现@@iterator方法时,就被认为是可迭代的。此外,这个方法返回迭代器。对象的@@iterator属性键可以通过Symbol.iterator访问。

让我们看一个例子来理解上面的陈述。

/*Start of an iterable protocol*/

const countries = {
    countryCollection: 
    ["Philippines", "Singapore", "Malaysia", "Canada", "Brazil", "Australia"],
    startIndex: 0,
    [Symbol.iterator]: function () {
        return {
            collection: this.countryCollection,
            index: this.startIndex,
            next: function () {
                if (this.index < this.collection.length) {
                    return {
                        value: this.collection[this.index++],
                        done: false
                    }
                } else {
                    return {
                        done: true
                    }
                }
            }
        }
    }
}

let countryIterator = countries[Symbol.iterator]();

console.log(countryIterator.next()); //output: {value: "Philippines", done: false}
console.log(countryIterator.next()); //output: {value: "Singapore", done: false}
console.log(countryIterator.next()); //output: {value: "Malaysia", done: false}
console.log(countryIterator.next()); //output: {value: "Canada", done: false}
console.log(countryIterator.next()); //output: {value: "Brazil", done: false}
console.log(countryIterator.next()); //output: {value: "Australia", done: false}
console.log(countryIterator.next()); //output: {done: true}

/**
 * Outputs: 
 * Philippines
 * Singapore
 * Malaysia
 * Canada
 * Brazil
 * Australia
 * undefined
 */
for (country of countries) {
    console.log(country)
}

/*End of an iterable protocol*/

现在我们对迭代器和可迭代协议有了初步的了解,以及for-of循环的作用。为什么不检查并学习内置的可迭代类型呢?

内置迭代器类型

JavaScript语言有内置的可迭代类型,但我们不会涵盖所有内容,而是将涵盖StringArraySet

内置可迭代类型的完整列表如下

  • 字符串
  • 数组
  • 映射
  • Set
  • TypedArray
  • Arguments Object

检查对象/类型是否实现了Symbol.iterator

/** Start of how to check if a type/object implements the Symbol.iterator */

let programmingLanguage = "JavaScript";

//let's us check if the built-in data-type String implements Symbol.iterator
//output: [Symbol(Symbol.iterator)]
Object.getOwnPropertySymbols(programmingLanguage.__proto__);

/** end of how to check if type/object implements the Symbol.iterator */

如你所见,上面的代码示例显示String数据类型确实实现了@@iterator。因此,Strings是可迭代的。

内置迭代器是什么样的?

让我们直接看一个例子,看看迭代器是什么样的。

//declare a JavaScript string type
let programmingLanguage = "JavaScript";

//lets invoke the method having an @@iterator property key
let iterator = programmingLanguage[Symbol.iterator]();

//let's check if the @@iterator property function return an iterator
console.log(iterator); //output: StringIterator

如你所见,在上面的示例中,我们声明了一个String类型并调用了@@iterator键符号,然后检查该函数是否确实返回了一个迭代器,它确实返回了一个StringIterator

内置迭代器类型的快速示例

我们不会深入细节,因为我们已经讨论了上面的概念。然而;这里的目标是展示我们如何与内置迭代器进行交互。

Array Iterator

@@iterator方法是可迭代协议的一部分,它定义了如何迭代项。

const pets = ["Andy", "Barkley", "Zandro"];
const petIterator = pets[Symbol.iterator]();
console.log(...petIterator); //outputs: Andy Barkley Zandro

String Iterator

[@@iterator]()方法返回一个迭代器,该迭代器迭代String值。

const fullName = "Jin Vincent Necesario";
const fullNameIterator = fullName[Symbol.iterator]();
console.log(...fullNameIterator); //outputs: J i n   V i n c e n t   N e c e s a r i o

Set Iterator

Set.prototype[@@iterator]()

@@iterator的初始值与values属性的初始值相同。

const myArraySet = new Set(['mango', 'avocado', 'apple', 'apple', 'mango']);
const myArraySetIterator = myArraySet[Symbol.iterator]();

//to show that the initial value of the @@iterator is the same as the initial values
console.log(myArraySet.values()); //output: SetIterator {"mango", "avocado", "apple"}
console.log(myArraySetIterator);  //output: SetIterator {"mango", "avocado", "apple"}

console.log(...myArraySetIterator); //outputs: mango avocado apple 

摘要

在这篇文章中,我们看到了JavaScript各种循环的优点和缺点。不仅如此,我们还讨论了迭代协议,它基本上有两种类型:迭代器协议和可迭代协议。最后,我们看到了内置迭代器是如何工作的。总而言之,这篇文章带你解决了迭代器这个难题。

希望您喜欢这篇文章,就像我喜欢写它一样。请继续关注更多内容。下次再见,祝您编程愉快!

历史

  • 2020年8月22日:初始版本
© . All rights reserved.