掌握迭代器读这一篇就够了

159 阅读13分钟

迭代器介绍

在 JavaScript 中,迭代器(Iterator)不,而是一种抽象的接口或协议。它提供了一种用于遍历集合(例如数组、对象等)中每个元素的统一方式。迭代器是一种使数据集合能够按顺序逐个访问其元素的机制,而无需了解底层数据结构的细节。

迭代器通常具有两个关键方法:

  1. next(): 这个方法用于返回迭代器中下一个元素的信息。它返回一个对象,包含两个属性:value 表示当前元素的值,done 表示是否已经迭代完所有元素,若迭代完成,则为 true
  2. Symbol.iterator: 这个方法是迭代器对象的特殊属性,它返回迭代器自身,使得迭代器可以在 for...of 循环中被使用。

迭代器的优点是它提供了一种抽象的方式来访问集合中的元素,而不需要关心集合内部的实现细节。这使得迭代器非常灵活,可以应用于各种数据结构,而不仅仅局限于数组或类数组对象。这也为 JavaScript 引入了一些新的遍历方法,例如 for...of 循环和 ... 操作符等。

可遍历的对象类型

类型/集合描述
数组 (Array)最常见的有序元素集合
字符串 (String)字符的有序集合
Map键值对的集合
Set不重复元素的集合
TypedArray二进制数据的集合
Arguments 对象函数参数的类数组对象
Generator 对象通过 Generator 函数生成的特殊对象,可进行惰性求值
DOM 集合文档对象模型中的一组 DOM 元素
Generator 函数生成器函数本身
异步迭代器可异步生成值的迭代器
自定义迭代器开发者自定义的迭代器
Node.js 核心模块返回对象例如 fs.readdir() 返回的可遍历的目录中文件名的迭代器

字符串可以被遍历,因为在 JavaScript 中,字符串被视为字符序列的有序集合。

数组迭代方法

方法

在 JavaScript 中,数组迭代方法有 forEachmapfilterreducesomeeveryfindfindIndex 等。这些方法都允许你在数组上执行不同类型的操作。下面是关于每个方法的简要说明:

  1. forEach: 对数组的每个元素执行指定操作,没有返回值。
  2. map: 对数组的每个元素执行指定操作,并返回操作结果组成的新数组。
  3. filter: 根据指定条件过滤数组中的元素,并返回符合条件的元素组成的新数组。
  4. reduce: 对数组的每个元素执行指定的归约操作(例如求和、累加等),并返回归约结果。
  5. some: 检查数组中是否至少有一个元素满足指定条件,返回布尔值。
  6. every: 检查数组中是否所有元素都满足指定条件,返回布尔值。
  7. find: 查找数组中第一个满足指定条件的元素,并返回该元素。
  8. findIndex: 查找数组中第一个满足指定条件的元素的索引,并返回该索引值。

注意细节

当使用数组迭代方法时,有一些重要的细节需要注意,特别是在处理空数组或者包含 undefinednull 等特殊值的数组时。以下是一些关于使用数组迭代方法时需要注意的重要细节:

  1. 处理空数组: 如果你的代码可能会处理空数组,需要考虑在使用数组迭代方法之前先进行空数组的判断。否则可能会出现意外的错误,比如尝试对空数组调用 forEachreduce 方法。

    const arr = [];
    
    // 使用数组迭代方法之前先判断数组是否为空
    if (arr.length > 0) {
        arr.forEach(item => console.log(item));
    } else {
        console.log('数组为空');
    }
    
  2. 处理 undefinednull 元素: 在对包含 undefinednull 元素的数组进行迭代时,可能需要注意这些特殊值的存在,并确保你的操作不会出现意外的行为。一种常见的方法是在使用迭代方法之前先对数组进行过滤。

    const arr = [1, undefined, 3, null, 5];
    
    // 使用 filter 方法过滤 undefined 和 null 元素
    const filteredArr = arr.filter(item => item !== undefined && item !== null);
    
    filteredArr.forEach(item => console.log(item));
    
  3. 返回值的处理: 对于一些数组迭代方法,特别是 mapreduce 等方法,需要注意它们的返回值,以确保你的代码逻辑正确。例如,map 方法返回一个新数组,而 reduce 方法返回一个归约后的值。

    const arr = [1, 2, 3, 4, 5];
    
    // 使用 map 方法将数组中的元素加倍
    const doubledArr = arr.map(item => item * 2);
    
    console.log(doubledArr); // 输出 [2, 4, 6, 8, 10]
    

最佳实践

在使用数组迭代方法时,需要注意处理原始数组的不变性、回调函数的纯函数性质、性能问题、异步操作和异常处理等方面的问题,以确保代码的正确性和可维护性。

  1. 原数组不变性: 大多数数组迭代方法(如 mapfilterreduce 等)都不会改变原始数组,而是返回一个新数组或者归约值。这意味着原始数组不会被修改,而是会返回一个经过处理后的新数组。如果需要修改原始数组,可以考虑使用 forEach 方法或者直接对原始数组进行操作。
  2. 回调函数的纯函数性质: 在使用数组迭代方法时,回调函数应该尽量保持纯函数的性质,即不修改外部状态,不产生副作用。这有助于提高代码的可维护性和可读性,并减少意外的行为。
  3. 性能考虑: 尽管数组迭代方法提供了方便的抽象和语法糖,但在处理大型数据集时可能会影响性能。特别是对于 forEachmapfilter 等方法,它们会创建新的数组,可能会占用大量的内存。在处理大型数据集时,需要考虑性能问题,可以尝试使用其他更高效的算法或者数据结构。
  4. 处理异步操作: 如果需要对数组中的元素进行异步操作,需要谨慎处理。在使用 forEach 方法时,无法等待异步操作的完成,因此可能需要使用 Promise.all 或者 async/await 来确保所有异步操作完成后再进行下一步处理。
  5. 异常处理: 如果回调函数中的操作可能会导致异常,需要考虑异常处理机制。特别是在 reduce 方法中,如果回调函数中发生异常,可能会导致整个归约过程中断,因此需要在回调函数中进行适当的异常处理。

特殊情况

除了前面提到的特殊情况之外,还有一些其他特殊情况需要考虑:

  1. 稀疏数组(Sparse Arrays): 稀疏数组指的是包含空位(undefined)的数组。在使用数组迭代方法时,空位会被跳过,而不会被当做有效元素处理。这可能会导致一些意外的行为,特别是在对数组进行一些数学运算时。

    const sparseArray = [, , 2, , 4];
    sparseArray.forEach((item, index) => console.log(index, item)); // 会跳过空位
    
  2. 数组长度的变化: 当在迭代过程中修改数组的长度时,可能会影响迭代的行为。一些迭代方法(如forEach)在开始迭代后不会受到数组长度的影响,而另一些方法(如mapfilter)则会受到影响。

    const arr = [1, 2, 3, 4, 5];
    arr.forEach((item, index) => {
      arr.pop(); // 删除末尾元素
      console.log(index, item); // 仍然会迭代所有元素
    });
    
  3. 对象属性迭代: 如果数组中的元素是对象,可能会在迭代时遇到一些问题,特别是在使用 mapfilter 等方法时。这时需要注意是否需要处理对象的引用问题。

    const arr = [{ value: 1 }, { value: 2 }, { value: 3 }];
    const newArr = arr.map(item => ({ value: item.value * 2 }));
    console.log(newArr); // 输出 [{ value: 2 }, { value: 4 }, { value: 6 }]
    
  4. NaN 和 undefined 的处理: 在使用一些条件判断的迭代方法(如 findsomeevery)时,需要注意 NaNundefined 的处理。例如,NaNundefined 在比较时不等于自身,因此可能会影响判断条件的结果。

    const arr = [1, NaN, 2, undefined, 3];
    const hasNaN = arr.some(item => isNaN(item));
    console.log(hasNaN); // 输出 true,因为数组中包含 NaN
    

对象迭代方法

JavaScript 中的对象(Object)是一种键值对的集合,它并没有直接提供类似数组的迭代方法。但是,可以通过一些方式来迭代对象的属性。

  1. for...in 循环: for...in 循环可以遍历对象的可枚举属性,包括自身的属性和原型链上的属性。注意,for...in 循环不保证属性的顺序。

    const obj = { a: 1, b: 2, c: 3 };
    for (const key in obj) {
      console.log(key, obj[key]);
    }
    
  2. Object.keys() 方法: Object.keys() 方法返回一个数组,包含对象的所有可枚举属性的键名。

    const obj = { a: 1, b: 2, c: 3 };
    const keys = Object.keys(obj);
    keys.forEach(key => console.log(key, obj[key]));
    
  3. Object.values() 方法: Object.values() 方法返回一个数组,包含对象的所有可枚举属性的值。

    const obj = { a: 1, b: 2, c: 3 };
    const values = Object.values(obj);
    values.forEach(value => console.log(value));
    
  4. Object.entries() 方法: Object.entries() 方法返回一个数组,包含对象的所有可枚举属性的键值对。

    const obj = { a: 1, b: 2, c: 3 };
    const entries = Object.entries(obj);
    entries.forEach(([key, value]) => console.log(key, value));
    

除了上述方法外,还可以使用其他一些技巧来迭代对象的属性:

  1. Object.getOwnPropertyNames() 方法: Object.getOwnPropertyNames() 方法返回一个数组,包含对象的所有属性(包括不可枚举属性)的键名。

    const obj = { a: 1, b: 2, c: 3 };
    const propertyNames = Object.getOwnPropertyNames(obj);
    propertyNames.forEach(propertyName => console.log(propertyName, obj[propertyName]));
    
  2. 使用 Symbol 迭代属性: 如果对象的键是 Symbol 类型,则可以使用 Object.getOwnPropertySymbols() 方法来获取所有 Symbol 属性的键名。

    const symbol1 = Symbol('a');
    const symbol2 = Symbol('b');
    const obj = { [symbol1]: 1, [symbol2]: 2 };
    const symbols = Object.getOwnPropertySymbols(obj);
    symbols.forEach(symbol => console.log(symbol, obj[symbol]));
    
  3. 使用 Reflect.ownKeys() 方法: Reflect.ownKeys() 方法返回一个数组,包含对象的所有属性的键名,包括字符串键和 Symbol 键。

    const symbol1 = Symbol('a');
    const obj = { a: 1, [symbol1]: 2 };
    const keys = Reflect.ownKeys(obj);
    keys.forEach(key => console.log(key, obj[key]));
    

这些方法可以帮助你迭代对象的属性,并对它们进行操作。

注意细节

for...in 循环会遍历对象的原型链上的属性,而其他方法只会遍历对象自身的属性。

  1. 遍历顺序不确定性: 对象属性的遍历顺序是不确定的。在 JavaScript 标准中,并没有规定对象属性的遍历顺序,不同的 JavaScript 引擎可能会有不同的实现。因此,在迭代对象属性时,不应该依赖于属性的遍历顺序。

  2. 原型链上的属性: for...in 循环会遍历对象的原型链上的所有可枚举属性,而其他方法(如 Object.keys()Object.values()Object.entries())只会遍历对象自身的可枚举属性。在某些情况下,遍历原型链上的属性可能会导致意外的行为,需要注意。

    function Parent() {
      this.parentProp = 'parent';
    }
    Parent.prototype.parentMethod = function() {
      console.log('Parent method');
    };
    function Child() {
      this.childProp = 'child';
    }
    Child.prototype = Object.create(Parent.prototype);
    const child = new Child();
    
    for (const key in child) {
      console.log(key); // 输出 'child' 和 'parentProp',但不会输出 'parentMethod'
    }
    
  3. 对象属性的可枚举性: 有些对象属性可能是不可枚举的,这意味着它们不会被 for...in 循环和一些 Object 方法遍历到。需要注意的是,大多数内置对象的原型属性是不可枚举的,但自定义属性通常是可枚举的。

    const obj = {};
    Object.defineProperty(obj, 'nonEnumerableProp', {
      value: 'non-enumerable',
      enumerable: false
    });
    for (const key in obj) {
      console.log(key); // 不会输出 'nonEnumerableProp'
    }
    
  4. 对象属性的特殊性: 有些属性具有特殊行为,如 __proto__ 属性、constructor 属性等。在遍历对象时,可能需要注意这些特殊属性的存在,以避免产生意外的结果。

    const obj = { a: 1, b: 2 };
    obj.__proto__.c = 3; // 修改原型链上的属性
    for (const key in obj) {
      console.log(key); // 输出 'a', 'b',但也会输出 'c',因为 c 是原型链上的属性
    }
    

当遍历对象时,需要注意遍历顺序的不确定性、原型链上的属性、对象属性的可枚举性以及特殊属性的存在等情况,以确保代码的正确性和可靠性。

中断迭代

在 JavaScript 中,breakreturn 语句通常用于中断迭代的循环结构,包括以下几种情况:

  1. for 循环: 使用 for 循环时,可以在循环体内使用 breakreturn 来中断迭代。

    for (let i = 0; i < 10; i++) {
      if (i === 5) {
        break; // 中断循环
      }
      console.log(i);
    }
    
  2. while 循环: 在 while 循环中同样可以使用 breakreturn 来中断迭代。

    let i = 0;
    while (i < 10) {
      if (i === 5) {
        break; // 中断循环
      }
      console.log(i);
      i++;
    }
    
  3. do...while 循环: 在 do...while 循环中也可以使用 breakreturn 来中断迭代。

    let i = 0;
    do {
      if (i === 5) {
        break; // 中断循环
      }
      console.log(i);
      i++;
    } while (i < 10);
    
  4. forEach 方法: 在数组的 forEach 方法中,使用 return 可以中断当前迭代,并继续下一次迭代。

    const arr = [1, 2, 3, 4, 5];
    arr.forEach(element => {
      if (element === 3) {
        return; // 中断当前迭代
      }
      console.log(element);
    });
    

除了上述情况,其他迭代方式如 for...of 循环等是无法使用 breakreturn 中断迭代的,因为它们没有明确的迭代控制结构。

forEach 限制

其中 需要注意的一些关于 forEach 的限制

forEach是用于数组的方法,它允许你在数组的每个元素上执行一个指定的操作

  1. 无法中断: forEach方法无法像for循环那样中途中断循环。这意味着你不能在forEach循环中使用breakreturn语句来提前退出循环。如果你需要在中途退出循环,可能需要考虑使用其他迭代方式,比如for循环。或者真要强行退出可以 throw Error 或者 使用 splice 清空数组,使下一次 next 的 done 为 true。
  2. 无法修改原始数组: 在forEach循环中,你可以访问数组的每个元素,但是你不能修改原始数组。如果你试图在forEach循环中修改数组,修改将不会被保留。正确方式 arr[index] 或者 arr.splice(index,0,"xxx")的方式修改。
  3. 返回值为undefined: forEach方法没有返回值(或者说返回的是undefined),因此你无法从forEach方法中获取任何有用的返回值。如果你需要在循环中收集某些数据或者执行某些操作后返回结果,可能需要使用其他方法。
  4. 适用于每个元素的回调函数: forEach方法接受一个回调函数作为参数,该函数将在数组的每个元素上调用。这个回调函数可以接受三个参数:当前元素值、当前元素的索引和原始数组本身。

结语

迭代器是 JavaScript 中强大且灵活的工具,通过深入了解迭代器的概念和使用方法,我们可以更好地处理各种数据集合。从介绍迭代器的基本概念开始,到探讨可遍历的对象类型、数组迭代方法以及对象迭代方法,我们逐步拓展了对迭代器的理解。在此过程中,我们强调了注意细节和特殊情况的重要性,并提供了一些最佳实践,帮助开发者编写出更优雅、可维护的代码。在技术的探索和应用中,持续学习和分享是不可或缺的,如果你有独特的见解或者经验,欢迎在下方评论区分享,让大家共同学习进步。