前端分享--ES6之Iterator(详解)

413 阅读15分钟

简介

Iterator 是 ES6 引入的一种新的遍历机制,迭代器有两个核心概念:

  • 迭代器是一个统一的接口,它的作用是使各种数据结构可被便捷的访问,它是通过一个键为Symbol.iterator 的方法来实现。
  • 迭代器是用于遍历数据结构元素的指针(如数据库中的游标)。

迭代过程

迭代的过程如下:

  • 通过 Symbol.iterator 创建一个迭代器,指向当前数据结构的起始位置
  • 随后通过 next 方法进行向下迭代指向下一个位置, next 方法会返回当前位置的对象,对象包含了 value 和 done 两个属性, value 是当前属性的值, done 用于判断是否遍历结束
  • 当 done 为 true 时则遍历结束

下面通过一个简单的例子进行说明:

//Array
const items = ["zero", "one", "two"]; 
const it = items[Symbol.iterator](); 
it.next(); 
>{value: "zero", done: false} 
it.next();
>{value: "one", done: false} 
it.next(); 
>{value: "two", done: false} 
it.next(); 
>{value: undefined, done: true}
//Map
const map = new Map(); 
map.set("a",1);
map.set("b",2);
map.set("c",3);
const it = map[Symbol.iterator](); 
it.next(); 
>{value: Array(2), done: false}
it.next();
>{value: Array(2), done: false}
it.next(); 
>{value: Array(2), done: false}
it.next(); 
>{value: undefined, done: true}

可迭代的数据结构

以下是可迭代的值:

  • Array

  • String

  • Map

  • Set

  • NodeList(document.getElementsByName("xxx")的返回值)

    1. NodeList是一中类数组对象,用于保存一组有序的节点
    2. 可以通过方括号来访问NodeList的值,他有item()方法与length属性。
    3. 他并不是Array的实列,没有数组对象的方法。
  • arguments

  • Generator

什么是 for…of 循环

ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一方法。

一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

for...of 语句创建一个循环来迭代可迭代的对象。在 ES6 中引入的 for...of 循环,以替代 for...in 和 forEach() ,并支持新的迭代协议。for...of 允许你遍历 Arrays(数组), Strings(字符串), Maps(映射), Sets(集合),DOM NodeList 对象,arguments对象,Generator 对象

语法: for (variable of iterable) { statement }

eg:几个常见的遍历

1:Maps(映射)

Map 对象就是保存 key-value(键值) 对。对象和原始值可以用作 key(键)或 value(值)。Map 对象根据其插入方式迭代元素。换句话说, for...of 循环将为每次迭代返回一个 key-value(键值) 数组。

//map-example.js 
const iterable = new Map([['one', 1], ['two', 2]]);   
for (const [key, value] of iterable) { 
    console.log(`Key: ${key} and Value: ${value}`); 
}   
// Output: 
// Key: one and Value: 1 
// Key: two and Value: 2

2:Set(集合)

Set(集合) 对象允许你存储任何类型的唯一值,这些值可以是原始值或对象。 Set(集合) 对象只是值的集合。 Set(集合) 元素的迭代基于其插入顺序。 Set(集合) 中的值只能发生一次。如果您创建一个具有多个相同元素的 Set(集合) ,那么它仍然被认为是单个元素。

// set-example.js 
const iterable = new Set([1, 1, 2, 2, 1]);   
for (const value of iterable) { 
    console.log(value); 
} 
// Output: 
// 1 
// 2

尽管我们的 Set(集合) 有多个 1 和 2 ,但输出的只有 1 和 2 。

3:Arguments Object(参数对象)

把一个参数对象看作是一个类数组(array-like)对象,并且对应于传递给函数的参数。这是一个用例:

// arguments-example.js 
function args() { 
    for (const arg of arguments) { 
        console.log(arg); 
    } 
}   

args('a', 'b', 'c'); 
// Output: 
// a 
// b 
// c

你可能会想,发生了什么事?! 如前所述,当调用函数时,arguments 会接收传入 args() 函数的任何参数。所以,如果我们传递 20 个参数给 args() 函数,我们将打印出 20 个参数

4:Generators(生成器)

生成器是一个函数,它可以退出函数,稍后重新进入函数。

// generator-example.js 
function* generator(){ 
    yield 1; yield 2; yield 3; 
};   
for (const g of generator()) { 
    console.log(g); 
}   

// Output: 
// 1 
// 2 
// 3

function* 定义了一个生成器函数,该函数返回生成器对象(Generator object)。

5:非 Iterator 的类数组对象

并不是所有类似数组的对象都具有 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);
}

6:for...of 遍历 对象

对于普通的对象,for...of结构不能直接使用,会报错。使用 for...in 可以遍历对象的键名。

let obj = {
    a: 1,
    b: 2,
    c: 3
}

for (let e in obj) {
    console.log(e);  // 'a'  'b'  'c'
}

至于对象没有布置iterator接口的原因,是因为数组,Map等结构中的成员都是有顺序的,即都是线性的结构,而对象,各成员并没有一个确定的顺序,所以遍历时先遍历谁后遍历谁并不确定。所以,给一个对象部署iterator接口,其实就是对该对象做一种线性转换。如果你有这种需要,就需要手动给你的对象部署iterator接口 如:

let obj = {
    data: [ 'hello', 'world' ],
    [Symbol.iterator]() {
        const self = this;
        let index = 0;
        return {
            next() {
                if (index < self.data.length) {
                    return {
                        value: self.data[index++],
                        done: false
                    };
                } else {
                    return { value: undefined, done: true };
                }
            }
        };
    }
};

另外,我们可以使用ES6的新特性——Object.entries()Object.keys()Object.values()方法将对象转换成可迭代对象,然后再使用for...of循环语句进行遍历。

例如,我们可以使用Object.entries()方法将对象转换成一个由键值对组成的数组,然后使用for...of循环语句对数组进行遍历,如下所示:

const obj = { a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
// 输出:
// "a" 1
// "b" 2
// "c" 3

其中,Object.entries()方法返回一个由键值对数组组成的数组,每个键值对数组包含两个元素,第一个元素为属性名(即键),第二个元素为属性值,所以我们可以使用解构赋值来获取每个键值对数组中的键和值。 对于遍历对象,Object.entries()、Object.keys()、Object.values()这三种方式各有优劣,具体选择取决于你的需求。

  • Object.entries(): 返回一个包含对象自身可枚举属性的键值对数组。适合需要同时访问键和值的情况,例如需要对对象进行键值对的遍历和操作。
const obj = { a: 1, b: 2, c: 3 };
for (const [key, value] of Object.entries(obj)) {
  console.log(key, value);
}
  • Object.keys(): 返回一个包含对象自身可枚举属性的键的数组。适合需要只访问对象的键而不需要值的情况。
const obj = { a: 1, b: 2, c: 3 };
for (const key of Object.keys(obj)) {
  console.log(key);
}
  • Object.values(): 返回一个包含对象自身可枚举属性的值的数组。适合需要只访问对象的值而不需要键的情况。
const obj = { a: 1, b: 2, c: 3 };
for (const value of Object.values(obj)) {
  console.log(value);
}

如果需要同时访问对象的键和值,则使用Object.entries();如果只需要访问对象的键,使用Object.keys();如果只需要访问对象的值,则使用Object.values()。根据具体的需求选择最合适的方法进行遍历。 把对象转换成可迭代对象

原理:一个普通对象成为可迭代对象时,必须包含一个 Symbol.iterator 属性,并规定是一个无参数的函数,其返回值为一个符合 迭代器协议 的对象。

第一种方式:

let obj = {
    name:"shishi",
    age:18,
    hometown:"xian",
    [Symbol.iterator]:function(){
        let keys = Object.keys(this);
        let length =keys.length;
        let cur = 0;
        return {
            next:()=>{
                return {
                    value:this[keys[cur]],
                    done:cur++ === length
                }
            }
        }
    }
}

for(let o of obj){
    console.log(o)
}

VM1149:2 shishi
VM1149:2 18
VM1149:2 xian

第二种方式:

let obj = {
  *[Symbol.iterator](){
    yield "sjl";
    yield "18";
    yield "xian";
  },
};

for(let o of obj){
    console.log(o)
}

VM1149:2 sjl
VM1149:2 18
VM1149:2 xian

For…of vs For…in

for...of 循环仅适用于迭代。 而普通对象不可迭代。

for...in 循环将遍历对象的所有可枚举属性。

//for-in-example.js 
Array.prototype.newArr = () => {}; 
Array.prototype.anotherNewArr = () => {}; 
const array = ['foo', 'bar', 'baz'];   
for (const value in array) { 
    console.log(value); 
} 
// Outcome: 
// 0 
// 1 
// 2 
// newArr 
// anotherNewArr

for...in 不仅枚举上面的数组声明,它还从构造函数的原型中查找继承的非枚举属性,在这个例子中,newArr 和 anotherNewArr 也会打印出来。

For…of vs ForEach

for of的return

function foo() {
  const arr = [1, 2, 3];
  for (const val of arr) {
    if (val === 2) {
      return false;
    }
    console.log(val)
  }
  // 这里的代码不会被执行
  console.log('我只是在循环之后输出的');
}
foo()  //1 false

forEach的return (return false相当于continue)

function foo() {
  const arr = [1, 2, 3];
  arr.forEach(ele=>{
    if (ele === 2) {
      return false;
    }
    console.log(ele)
  })
  // 这里的代码会被执行
  console.log('我只是在循环之后输出的');
}
foo()  //1 3 我只是在循环之后输出的

如果要遍历 index

let arr = [1, 2, 3, 4]
for(let [index, value] of arr.entries()) {
  arr[index] = value * 10
}
console.log(arr) // [ 10, 20, 30, 40 ]
let arr = [1, 2, 3, 4]
arr.forEach((ele,index)=>{
  arr[index] = value * 10
})
console.log(arr) // [ 10, 20, 30, 40 ]

ps:

forEach 循环和一般的 for 循环、while 循环以及 for...of 循环是有区别的。在 forEach 循环中,我们传递的是一个回调函数,这个回调函数对数组中的每一个元素都会被执行一遍。因为回调函数是在循环体内执行的,所以我们不能直接使用 break 语句来跳出整个循环体。

相反,在 for 循环、while 循环和 for...of 循环中,我们是直接操作循环变量来控制循环的条件和循环的执行流程的。因此,我们可以在循环体内使用 break 和 continue 来控制循环的执行流程。

需要注意的是,如果在带有块级作用域的函数中使用 for 循环或 while 循环,那么可以使用 return 语句来直接结束整个函数的执行并返回结果。但是如果使用 forEach 循环,那么同样无法直接使用 return 语句结束整个函数的执行。如果我们需要在 forEach 循环中结束整个函数的执行并返回结果,可以使用抛出异常的方式来实现。

当使用 forEach 循环遍历学生列表,如果找到了特定学生的信息,就提前结束整个函数的执行并返回结果。

// 定义学生列表
const students = [
  { id: 1, name: 'Alice' },
  { id: 2, name: 'Bob' },
  { id: 3, name: 'Charlie' },
  { id: 4, name: 'David' },
  { id: 5, name: 'Eve' }
];

// 定义函数,根据学生姓名查找学生信息
function findStudentByName(name) {
  let result = null;

  try {
    students.forEach((student) => {
      if (student.name === name) {
        // 在找到学生信息时抛出异常并设置结果
        result = student;
        throw new Error('StopIteration');
      }
    });
  } catch (error) {
    if (error.message !== 'StopIteration') {
      throw error; // 如果不是预期的异常,重新抛出异常
    }
  }

  return result; // 返回结果
}

// 调用函数并获取结果
const foundStudent = findStudentByName('Charlie');
console.log(foundStudent);

综上所述,虽然 forEach 循环在编写代码时非常方便,但是在需要控制循环执行流程时,使用传统的 for 循环、while 循环或者 for...of 循环可能会更好

ps: forEach 切记不能在有async await的代码里使用,不会起到同步的效果

for 循环和 forEach 方法在处理异步操作上的差异主要是因为在事件循环中的执行方式不同。

  1. for 循环

    • for 循环是一种基本的迭代结构,它允许你手动控制迭代过程。
    • 当你在 for 循环中使用 async/await 时,每次迭代都会等待异步操作完成,然后再执行下一次迭代。这是因为 for 循环是同步执行的,它会在当前事件循环中逐步执行每次迭代,每次迭代都会等待异步操作完成后再进行下一步操作。
  2. forEach 方法

    • forEach 方法是数组原型对象的方法,它是一种高阶函数,内部已经封装好了迭代逻辑。
    • forEach 方法会在当前事件循环中一次性执行所有的回调函数,并不会等待回调函数中的异步操作完成。这意味着即使在回调函数中使用了 async/awaitforEach 也不会等待异步操作完成再进行下一次迭代,而是会继续执行下一个回调函数。

看一个例子:

async function test() {
    let arr = [321]
    arr.forEach(async item => {
        const res = await mockSync(item)
        console.log(res)
    })
    console.log('end')
}

function mockSync(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
                resolve(x)
        }, 1000 * x)
    })
}
test()
我们期望的结果是:
3
2 
1
end

但是实际上会输出:

end
1
2
3

JavaScript中的forEach()方法是一个同步方法,它不支持处理异步函数。如果你在forEach中执行了异步函数,forEach()无法等待异步函数完成,它会继续执行下一项。这意味着如果在forEach()中使用异步函数,无法保证异步任务的执行顺序。

替代forEach的方式

1.方式一

可以使用例如map()filter()reduce()等,它们支持在函数中返回Promise,并且会等待所有Promise完成。

使用map()Promise.all()来处理异步函数的示例代码如下:

const arr = [12345];

async function asyncFunction(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * 2);
    }, 1000);
  });
}

const promises = arr.map(async (num) => {
  const result = await asyncFunction(num);
  return result;
});

Promise.all(promises).then((results) => {
  console.log(results); // [2, 4, 6, 8, 10]
});

由于我们在异步函数中使用了await关键字,map()方法会等待异步函数完成并返回结果,因此我们可以正确地处理异步函数。

方式二 使用for循环来处理异步函数

const arr = [12345];

async function asyncFunction(num) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(num * 2);
    }, 1000);
  });
}

async function processArray() {
  const results = [];
  for (let i = 0; i < arr.length; i++) {
    const result = await asyncFunction(arr[i]);
    results.push(result);
  }
  console.log(results); // [2, 4, 6, 8, 10]
}

processArray();

什么是 Generator函数

Generator函数是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。 语法上,可以把理解成,Generator 函数是一个状态机,封装了多个内部状态。 形式上,Generator 函数是一个普通函数。它不同于普通函数,是可以暂停执行的,所以函数名之前要加星号 *,以示区别。

整个Generator函数就是一个封装的异步任务,或者说是异步任务的容器,异步操作需要暂停的地方,都用yield语句。 调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。 next 方法的作用是分阶段执行 Generator 函数。每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和 done 属性)。value 属性是 yield 语句后面表达式的值,表示当前阶段的值;done 属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段(done为false 继续执行)。值得注意的是

当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined

示例如下:

function *foo(x) { 
    let y = 2 * (yield (x + 1)) 
    let z = yield (y / 3) 
    return (x + y + z) 
} 
let it = foo(5) 
console.log(it.next()) // => {value: 6, done: false} 
console.log(it.next(12)) // => {value: 8, done: false} 
console.log(it.next(13)) // => {value: 42, done: true}

上面这个示例就是一个Generator函数,我们来分析其执行过程:

  • 首先 Generator 函数调用时它会返回一个迭代器
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数等于上一个 yield 的返回值,如果你不传参,yield 永远返回 undefined。此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8
  • 当执行第三次 next 时,传入的参数会传递给 z,所以 z = 13, x = 5, y = 24,相加等于 42

Generator vs async

1、Generator 出现在ES2015中,async 出现在ES2017中,async 是 Generator 的语法糖

2、执行方式不同,Generator 执行需要使用执行器(next()等方法);async 函数自带执行器,与普通函数的执行一样;

3、async 的语法语义更加清楚,async 表示异步,await 表示等待;而 Generator 函数的(*)号和 yield 的语义就没那么直接了;

4、Generator 中 yield 后面只能跟 Thunk 函数或 Promise 对象;而 async 函数中 await 后面可以是 promise 对象或者原始类型的值(会自动转为立即resovle的promise对象);

5、返回值不同,Generator 返回遍历器,相比于 async 返回 promise 对象操作更加麻烦。

PS:

break 是退出当前循环(如果有多重for循环的情况)

return 是退出整个循环, 使用return会直接跳出函数(如果有多重for循环的情况)

在for中不能使用 return,不然会报错(除非循环是在函数里)

在Javascript中,一个函数一旦开始执行,就会运行到最后或遇到return时结束,运行期间不会有其它代码能够打断它,也不能从外部再传入值到函数体内

而Generator函数(生成器)的出现使得打破函数的完整运行成为了可能,其语法行为与传统函数完全不同

Generator函数是ES6提供的一种异步编程解决方案,形式上也是一个普通函数,但有几个显著的特征:

function关键字与函数名之间有一个星号 "*" (推荐紧挨着function关键字) 

函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield) 

直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object) 

依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态