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





5.00/5 (4投票s)
学习不同循环结构的缺点,以及如何使用不同的迭代协议
引言
如果你已经使用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()
方法必须返回一个包含value
和done
属性的对象。
简而言之,当一个对象提供一个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语言有内置的可迭代类型,但我们不会涵盖所有内容,而是将涵盖String
、Array
和Set
。
内置可迭代类型的完整列表如下
字符串
数组
映射
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日:初始版本