迭代的英文“iteration”源自拉丁文itero,意思是“重复”或“再来”。在软件开发领域,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。 ECMAScript 6规范新增了两个高级特性:迭代器和生成器。使用这两个特性,能够更清晰、高效、方便地实现迭代。
1、理解迭代
在JavaScript中,计数循环就是一种最简单的迭代
for (let i = 1; i <= 10; ++i) {
console.log(i);
}迭代会在一个有序集合上进行,(“有序”可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。)数组是JavaScript中有序集合的最典型例子。
let collection = ["foo", "bar", "baz"];
for (let index = 0; index < collection.length; ++index) {
console.log(collection[index]);
}因为数组有已知的长度,且数组每一项都可以通过索引获取,所以整个数组可以通过递增索引来遍历。由于如下原因,通过这种循环来执行例程并不理想:
1、迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能先通过引用取得数组对象,然后再通过[]操作符取得特定索引位置上的项。这种情况并不适用于所有数据结构。 2、遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。
ES5新增了Array.prototype.forEach()方法,向通用迭代需求迈进了一步,这个方法解决了单独记录索引和通过数组对象取得值的问题。因此这个方法只适用于数组,而且回调结构也比较笨拙。
2、迭代器
迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,使用该接口无需关心对象的内部实现细节。
其行为像数据库中的光标,迭代器最早出现在1974年设计的CLU编程语言中;
在各种编程语言的实现中,迭代器的实现方式各不相同,但是基本都有迭代器,比如Java、Python等;
从迭代器的定义我们可以看出来,迭代器是帮助我们对某个数据结构进行遍历的对象。
在JavaScript中,迭代器也是一个具体的对象,这个对象需要符合迭代器协议(iterator protocol):迭代器协议定义了产生一系列值(无论是有限还是无限个)的标准方式;在js中这个标准就是一个特定的next方法;
next方法有如下的要求: 一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
done(boolean)
如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定 done 这个属性。)如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
value
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
迭代器代码练习:
let names = ["sun", "james", "lebron"];
let index = 0;
let iterator = {
next: function () {
if (index < names.length) {
return {
done: false,
value: names[index++],
};
} else {
return {
done: true,
value: undefined,
};
}
},
};
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
// { done: false, value: "sun" }
// { done: false, value: "james" }
// { done: false, value: "lebron" }
// { done: true, value: undefined }
// { done: true, value: undefined }let names = ["sun", "james", "lebron"];
function createArrayIterator(arr) {
let index = 0;
return {
next: function () {
if (index < arr.length) {
return { done: false, value: arr[index++] };
} else {
return { done: true, value: undefined };
}
},
};
}
const iterator = createArrayIterator(names);
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());可迭代对象但是上面的代码整体来说看起来是有点奇怪的:
迭代一个数组的时候,需要自己创建一个index变量,再创建一个所谓的迭代器对象;
事实上我们可以对上面的代码进行进一步的封装,让其变成一个可迭代对象;
当一个对象实现了可迭代协议时,它就是一个可迭代对象;这个对象的要求是必须实现 @@iterator 方法,在代码中我们使用 Symbol.iterator 访问该属性;
代码演示:
const iterableObj = {
names: ["sun", "james", "lebron"],
[Symbol.iterator]: function () {
let index = 0;
return {
next: () => {
if (index < this.names.length) {
return { done: false, value: this.names[index++] };
} else {
return { done: true, value: undefined };
}
},
};
},
};
let iterator = iterableObj[Symbol.iterator]();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());转成这样的一个东西有什么好处呢?当一个对象变成一个可迭代对象的时候,进行某些迭代操作,比如 for…of 操作时,其实就会调用它的 @@iterator 方法;
原生迭代器对象平时创建的很多原生对象已经实现了可迭代协议,会生成一个迭代器对象的:String、Array、Map、Set、arguments对象、NodeList集合;
可迭代对象的应用1、JavaScript中语法:for …of、展开语法(spread syntax)、yield*(后面讲)、解构赋值(Destructuring_assignment); 2、创建一些对象时:new Map([Iterable])、new WeakMap([iterable])、new Set([iterable])、new WeakSet([iterable]); 3、一些方法的调用:Promise.all(iterable)、Promise.race(iterable)、Array.from(iterable);
let names = {
name1: "sun",
name2: "james",
name3: "lebron",
};
console.log(...iterableObj); //sun james lebron
console.log(...names); //TypeError: Found non-callable @@iterator
console.log({ ...names });
//{ name1: "sun", name2: "james", name3: "lebron" } es9实现,用的不是可迭代对象
//创建一些对象
const set = new Set(iterableObj);
console.log(set); //Set(3) { "sun", "james", "lebron" }自定义类的迭代在面向对象开发中,我们可以通过class定义一个自己的类,这个类可以创建很多的对象:如果我们也希望自己的类创建出来的对象默认是可迭代的,那么在设计类的时候我们就可以添加上@@iterator 方法;
class ClassRoom {
constructor(name, address, students) {
(this.name = name), (this.address = address), (this.students = students);
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.students.length) {
return { done: false, value: this.students[index++] };
} else {
return { done: true, value: undefined };
}
},
};
}
}
const r1 = new ClassRoom(301, "8e", ["sun", "james", "lebron"]);
for (let s of r1) {
console.log(s);
}迭代器的中断迭代器在某些情况下会在没有完全迭代的情况下中断:比如遍历的过程中通过break、continue、return、throw中断了循环操作;比如在解构的时候,没有解构所有的值;想要监听中断的话,可以添加return方法:
class ClassRoom {
constructor(name, address, students) {
(this.name = name), (this.address = address), (this.students = students);
}
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
if (index < this.students.length) {
return { done: false, value: this.students[index++] };
} else {
return { done: true, value: undefined };
}
},
return() {
console.log("监听到迭代停止");
return {
done: true,
};
},
};
}
}
const r1 = new ClassRoom(301, "8e", ["sun", "james", "lebron"]);
for (let s of r1) {
console.log(s);
if (s === "james") {
break;
}
}3、生成器
生成器是ES6中新增的一种函数控制、使用的方案,它可以让我们更加灵活的控制函数什么时候继续执行、暂停执行等。平时我们会编写很多的函数,这些函数终止的条件通常是返回值或者发生了异常。
生成器函数也是一个函数,但是和普通的函数有一些区别: 1、生成器函数需要在function的后面加一个符号:* 2、生成器函数可以通过yield关键字来控制函数的执行流程 3、生成器函数的返回值是一个Generator(生成器),生成器事实上是一种特殊的迭代器
生成器函数执行
function* say() {
console.log(1);
yield;
console.log(2);
console.log(3);
yield 5;
console.log(4);
}
const generator = say();
console.log(generator.next());
console.log(generator.next());
generator.next();
// 1
// { value: undefined, done: false }
// 2
// 3
// { value: 5, done: false }
// 4生成器函数say返回一个生成器对象(这个生成器对象本质上是一个特殊的迭代器对象),通过调用next方法控制函数执行。
next方法是有返回值的:
{ value: undefined, done: false }如果不希望next方法返回这个对象,可以通过 yield value来实现。
生成器传递参数 – next函数在调用next函数的时候,可以给它传递参数,那么这个参数会作为上一个yield语句的返回值
function* say() {
console.log(1);
const n = yield;
console.log(n);
}
const generator = say();
generator.next();
generator.next(2);
// 1
// 2生成器提前结束 – return函数还有一个可以给生成器函数传递参数的方法是通过return函数:return传值后这个生成器函数就会结束,之后调用next不会继续生成值了
function* foo() {
console.log(1);
const value1 = yield "sun";
console.log("value1: ", value1);
const value2 = yield value1;
//不会执行以下代码
const value3 = yield value2;
}
const generator = foo();
console.log(generator.next());
console.log(generator.return(123));
console.log(generator.next());生成器抛出异常 – throw函数除了给生成器函数内部传递参数之外,也可以给生成器函数内部抛出异常:抛出异常后我们可以在生成器函数中捕获异常;但是在catch语句中不能继续yield新的值了,但是可以在catch语句外使用yield继续中断函数的执行。
function* foo() {
console.log(1);
try {
yield 1;
} catch (error) {
console.log(error);
}
console.log(2);
}
const generator = foo();
generator.next();
generator.throw("error:error message");生成器替代迭代器因为生成器函数调用next方法返回一个{value:string,done:boolean}对象,同样迭代器函数需要返回的也是。生成器是一种特殊的迭代器,那么在某些情况下我们可以使用生成器来替代迭代器,还可以使用yield*来生产一个可迭代对象:候相当于是一种yield的语法糖,只不过会依次迭代这个可迭代对象,每次迭代其中的一个值;
function* createArrayIterator(arr) {
let index = 0;
yield arr[index++];
yield arr[index++];
yield arr[index++];
}
// function* createArrayIterator(arr) {
// for (let item of arr) {
// yield item;
// }
// }
// function* createArrayIterator(arr) {
// yield* arr;
// }