1. for...of 循环
1. 现有的各种循环存在的问题
- for,写法上比较繁琐
- forEach,无法使用 break 或 return 跳出循环;和其他循环不同,它是一个函数
- for...in,不仅遍历数字键名,还会遍历其他键;遍历出的键是字符串,不是数字类型。for...in 更适合遍历对象。
所以为了解决这些问题,ES6新增了一个for...of循环,写法简洁,又没有其他循环的缺点。
let arr = ['red', 'green', 'blue', null];
arr.color = 'default';
for (var i=0; i<arr.length; i++) {
console.log(arr[i]); //'red' 'green' 'blue' null
}
arr.forEach((value, key) => {
// if (value === null) break; //SyntaxError: Illegal break statement
console.log(key, value);
//0 'red'
//1 'green'
//2 'blue'
//3 null
})
for (var index in arr) {
console.log(index); //'0' '1' '2' '3' 'color'
}
for (var value of arr) {
if (value === null) break;
console.log(value) //'red' 'green' 'blue'
}
2. 使用 for...of
想使用for...of 循环遍历数据,需要这个数据结构满足一个特点,就是它需要具备 Iterator 接口。 也就是说 for...of 循环是遍历所有具备 Iterator 接口数据结构的统一的方法。
数组、Set 和 Map 结构、arguments、DOM NodeList 对象、字符串、以及 Generator 对象。
以上数据结构原生具备 Iterator 接口,Object并不具备
这是因为 for...of 循环是一种线性循环,是按照一定的输出顺序遍历的
所以对象更适合的遍历方式是 for...in,如果想使用for...of ,就需要手动添加 Iterator 接口。
2. Iterator 遍历器
1. 概念
Iterator(遍历器)是一种接口机制,用来处理不同的数据结构。用来供for...of消费使用。
遍历器本质是一个指针对象,对象里有一个 next
方法,指针最开始指向数据的起始位置,每调用一次next方法,指针向后移动一个位置,并返回一个对象,包含value和done两个属性。
{
next: function () {
if (/*条件*/) {
return {value: 123, done: false}
}
return {value: undefined, done: true};
}
}
2. 默认 Iterator 接口
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性。
只要一个数据结构定义了一个 Symbol.iterator 的属性,这个属性是一个函数,并且返回一个带有 next 方法的对象,那么这个数据就是可遍历的(iterable),可以被for...of遍历。
let arr = ['a', 'b', 'c'];
let it = arr[Symbol.iterator]();
console.log(it.next()) //{ value: 'a', done: false }
console.log(it.next()) //{ value: 'b', done: false }
console.log(it.next()) //{ value: 'c', done: false }
console.log(it.next()) //{ value: undefined, done: true }
3. 调用 Iterator 接口的场合
- for...of
- 解构赋值
- 扩展运算符
let set = new Set().add('a').add('b').add('c');
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all(), Promise.race()
- yield*
4. return(),throw()
遍历器除了可以定义next方法(必需),还可以定义return和throw方法(非必需)。
3. Generator 生成器
1. 与普通函数的区别
在写法上有两点区别,一是在function关键字后面多了一个 *;二是函数内部有yield关键字。
yield 字段只能在 Generator 内部使用,它能使 Generator 函数“暂停”。
在使用上有很大的不同, Generator 函数调用后并不会马上执行,而是返回一个对象,每调用一次对象上的next方法,会产出一个数据。
Generator也可以理解成一个状态机,通过yield字段控制内部指针指向。
function* gen () {
yield 'hello';
yield 'world';
return 'ending!';
}
let it = gen();
console.log(it.next()) //{ value: 'hello', done: false }
console.log(it.next()) //{ value: 'world', done: false }
console.log(it.next()) //{ value: 'ending', done: true }
2. 与 Iterator 的关系
可以看到上面调用生成器函数gen后返回的是一个带有next方法的对象,这个对象就是一个 Iterator。
Generator 函数就是遍历器生成函数,调用 Generator 函数会返回一个遍历器 (Iterator) 。
因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
//...
var myIterable = {};
myIterable[Symbol.iterator] = gen;
console.log([...myIterable]) //[ 'hello', 'world' ]
需要注意的是myIterable只有两个数据,并没有ending,因为它的done是true,同样for...of也不会遍历到ending。
3. next 参数
一般情况下,yield的返回值是undefined,如果next方法有参数,yield的返回值就是这个参数的值。
function* f(count) {
let x = yield count;
let y = x + (yield x);
yield y + (x + (yield x + y));
return 'ending';
}
var g = f(10);
console.log(g.next()) //{ value: 10, done: false }
console.log(g.next(1)) //{ value: 1, done: false }
console.log(g.next(2)) //{ value: 4, done: false }
console.log(g.next(3)) //{ value: 7, done: false }
console.log(g.next(4)) //{ value: 'ending', done: true }
- 第一次调用next不能传参。
- 第二次调用传了 1,并且赋值给x,然后产出了count为10
- 第三次调用传了 2,并且赋值到运算公式(let y = x + (yield x) => let y = 1 + 2),然后产出了x为1
- 依次调用下去,最后一次虽然传了值但是最后yield并没有赋值操作,也就没有用到。 从这个例子中可以看到几个特点
- (1). Generator 函数遇到 yield 就会暂停产出一个结果。
- (2). next传参了,yield就有值,反之则是undefined
- (3). yield 参与运算只关心yield的值,并不关心yield后面产出的结果,假设上面第四次调用不传值,则会产出 NaN。
4. yield* 表达式
在一个 Generator 函数里调用另一个 Generator 函数,可以使用yield* 表达式。
function* gen1 () {
yield 1;
yield 2;
}
function* gen2 () {
yield 3;
yield* gen1();
yield 4;
}
for (let value of gen2()) {
console.log(value) //3 1 2 4
}
- yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
- 有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。
- yield* 后面可以执行任何一个带有 Iterator 接口的数据
function* gen () {
yield* new Set([3, 2, 1]);
}
console.log([...gen()]) //[ 3, 2, 1 ]
5. return(),throw()
自定义的遍历器必须包含next()方法,return()和throw()是可以不写的。 Generator函数生成的遍历器默认这3种方法都会有。
- return()方法可以返回给定的值,并且终结遍历 Generator 函数。遇到 try{}finally{}代码块会立即执行finally里面的逻辑,最后返回return的值。
function* gen () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
return 7;
}
let it = gen();
console.log(it.next()); //{ value: 1, done: false }
console.log(it.next()); //{ value: 2, done: false }
console.log(it.return(8)); //{ value: 4, done: false }
console.log(it.next()); //{ value: 5, done: false }
console.log(it.next()); //{ value: 8, done: true }
- throw()方法可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
function* gen() {
try {
yield console.log(1);
yield console.log(2);
yield console.log(3);
yield console.log(4);
} catch (e){
console.log('内部捕获', e)
}
yield console.log(5);
yield console.log(6);
yield console.log(7);
}
let it = gen();
try {
throw new Error('777')
it.throw('a'); //不会执行
} catch (e) {
console.log('throw命令抛出外部捕获',e) //捕获到 777
}
try {
it.next(); // 1
it.next(); // 2
it.throw('b'); // 内部捕获 b,并执行 yield 打印 5
it.next(); // 6
it.throw('c'); // 外部捕获 c
it.next(); // 不会执行了
it.next(); // 不会执行了
} catch(e) {
console.log('外部捕获', e)
}
// throw命令抛出外部捕获 Error: 777 ...
// 1
// 2
// 内部捕获 b
// 5
// 6
// 外部捕获 c
(1). throw命令抛出的错误只能在外部捕获,并且try代码块里的剩余代码不会再执行。
(2). throw()方法执行前至少执行前至少要执行一次next()方法。
(3). throw()方法抛出的错误想在函数内部捕获需要 Generator 函数内部用 try catch 包裹。
(4). throw()方法被内部捕获后 try 代码块剩余代码还可以继续执行。并且会相当于执行一次next
(5). throw()方法被外部捕获后,相当于用 throw 命令抛出错误,会终止 try 代码块内的代码。
next()、throw()、return()这三个方法本质上是同一件事,它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。