1. 迭代器介绍
在Javascript中,存在很多循环方法,例如数组的forEach
、map
、some
等等。还有更通用的for...in
以及for循环
。在ES6之前,我们常常使用这些方法完成循环的功能。但这些方法存在明显的缺陷:
- 我们需要记住各种各样的api来达到不同的需求。数组上的方法不可以用于遍历对象,尤其在ES6引入
Map
、Set
等后,如果再像数组那样将遍历方法放在Map
或者Set
上,会给开发者带来心智负担。因此需要一个统一的迭代方法,使得迭代更轻松。 - ES5的一些方法,例如
forEach
,使用起来容易令人误解,在forEach
中无法使用break
和continue
中断循环。
在Javascript中,可以使用for...of...
来使用迭代器:
javascript
const arr = [1, 2, 3];
for (const value of arr) {
console.log(value); // 依次打印1, 2, 3
}
和for循环
一样,在for...of...
中也可以使用continue
和break
来中断循环。
javascript
const arr = [1, 2, 3];
for (const value of arr) {
if (value > 2) break; // value为3时跳出循环
console.log(value); // 依次打印1, 2
}
在Javascript中,以下数据类型上存在迭代器:
- 数组
- 字符串
- Map
- Set
- NodeList
- arguments参数
除了for...of...
,还可以使用Array.from
、解构
、扩展运算符
等方式使用迭代器:
javascript
const nodes = Array.from(document.getElementsByTagName('a')); // 将NodeList转成了数组
const [a, b] = [1, 2];
console.log(a, b); // 打印1和2
function fn () {
console.log([...arguments]); //打印参数列表: [1, 2, 3]
}
fn(1, 2, 3)
2. 迭代器原理
所有迭代器均实现了Symbol.iterator
接口,判断一个数据类型是否能够使用迭代器,只需要判断在这个数据上是否存在这个接口即可。
javascript
// 判断一个数据是否可以使用迭代器
function isIterable (value) {
if (value == null) return false;
return typeof value[Symbol.iterator] === 'function';
}
const arr = [1, 2];
const obj = { a: 1, b: 2};
console.log(`arr能否使用迭代器:${isIterable(arr)}`); // arr能否使用迭代器:true
console.log(`obj能否使用迭代器:${isIterable(obj)}`); // obj能否使用迭代器:false
Symbol.iterator
函数返回一个对象(这个对象可以称为迭代器对象
),迭代器对象中必须存在一个next
方法,next
方法执行后返回一个对象,包含done
和value
两个属性。done
用来表示迭代是否结束,value
则是当前迭代的值。
javascript
const arr = [1, 2];
const iterator = arr[Symbol.iterator]();
console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: true, value: undefined }
// 迭代结束后,继续执行next始终返回相同的结果
console.log(iterator.next()); // { done: true, value: undefined }
从上面代码可以推断出,迭代器的迭代原理就是依次执行next
方法,直到done
为true时结束迭代。基于这个原理,我们可以给一些不存在内置迭代器的数据类型添加迭代器:
javascript
Object.prototype[Symbol.iterator] = function () {
const obj = this;
const keys = Object.keys(obj);
let i = 0;
return {
next () {
return {
done: i >= keys.length,
value: obj[keys[i ++]]
}
}
}
}
const obj = { a: 1, b: 2 };
for (const value of obj) {
console.log(value); // 依次打印1, 2
}
除了next
方法,迭代器对象中还可以存在return
方法,执行return
方法必须返回一个对象。大部分内置数据类型的迭代器中都不存在return
方法(后面要讲的生成器对象
中存在return
方法),当使用break
、continue
中断迭代,或者使用解构时,会触发return
方法执行(如果存在的话),也可以直接执行return
方法中断迭代。
javascript
const obj = {
a: 1,
b: 2,
[Symbol.iterator] () {
let i = 0;
const self = this;
const keys = Object.keys(self);
return {
next () {
return {
done: i >= keys.length,
value: self[keys[i ++]]
}
},
return () {
console.log('触发了return方法');
return { done: true, value: undefined } // 必须返回一个对象,否则会报错
}
}
}
};
for (const v of obj) {
if (v === 2) break; // 触发了return方法
}
const [a] = obj; // 触发了return方法
内置的迭代器对象中,也实现了迭代器接口,且迭代器方法返回的对象等于当前迭代器对象:
javascript
const arr = [1, 2];
const it = arr[Symbol.iterator]();
console.log(typeof it[Symbol.iterator] === 'function'); // true
console.log(it[Symbol.iterator]() === it); // true
因此也可以迭代这个对象遍历数组值:
javascript
const arr = [1, 2];
const it = arr[Symbol.iterator]();
for (const v of it) {
console.log(v); // 1, 2
}
如果中断了迭代,下次迭代从中断的下一个位置继续迭代:
javascript
const arr = [1, 2, 3, 4];
const it = arr[Symbol.iterator]();
let v;
for (v of it) {
console.log(v); // 1, 2
if (v === 2) break;
}
for (v of it) {
console.log(v); // 3, 4
}
3. 使用迭代器
- 访问
Set
、Map
的第一项:
javascript
const set = new Set([1, 2]);
const map = new Map([
['name', 'zhangsan'],
['age', 18]
]);
console.log(set[Symbol.iterator]().next().value); // 1
console.log(map.entries()[Symbol.iterator]().next().value); // ['name', 'zhangsan']
- 自定义迭代器
javascript
class Company {
#members = [];
constructor (name) {
this.name = name;
}
addMember (member) {
this.#members.push(member);
return this;
}
[Symbol.iterator] () {
let i = 0;
const self = this;
return {
next () {
return {
done: i >= self.#members.length,
value: self.#members[i ++]
}
}
}
}
}
const baidu = new Company('baidu');
baidu.addMember('zhangsan').addMember('lisi');
for (const member of baidu) {
console.log(member); // 'zhangsan', 'lisi'
}
4. 生成器介绍
生成器是一种可以暂停执行的函数,使用yield
关键字暂停执行函数,使用next
恢复函数执行:
javascript
function * fn () {
console.log('start');
yield 1;
console.log('step1');
yield 2;
console.log('step2');
yield 3;
console.log('end');
}
const gen = fn();
gen.next(); // 'start'
gen.next(); // 'step1'
gen.next(); // 'step2';
gen.next(); // 'end'
生成器函数返回一个生成器对象
,包含next
、return
和throw
方法。和迭代器一样,next
执行后返回一个包含done
和value
的对象,生成器函数的返回值会作为最后一次迭代结果的value
值:
javascript
function * fn () {
yield 1;
yield 2;
yield 3;
return 4;
}
const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.next()); // { done: false, value: 2 }
console.log(gen.next()); // { done: false, value: 3 }
console.log(gen.next()); // { done: true, value: 4 }
console.log(gen.next()); // { done: true, value: undefined }
生成器对象
也实现了迭代器接口:
javascript
function * fn () {
yield 1;
yield 2;
yield 3;
}
const gen = fn();
const it = gen[Symbol.iterator]();
console.log(gen === it); // true
for (const value of gen) {
console.log(value); // 依次打印1, 2, 3
}
next
函数接收一个参数,作为上一次yield
的返回值:
javascript
function * fn () {
console.log(yield 1);
console.log(yield 2);
console.log(yield 3);
}
const gen = fn();
gen.next(0); // 首次执行,没有上一个yield,因此参数被忽略
gen.next(1); // 打印1
gen.next(2); // 打印2
gen.next(3); // 打印3
return
函数可以中断迭代,接受一个参数,作为return
函数的返回值中的value
:
javascript
function * fn () {
console.log(yield 1);
console.log(yield 2);
console.log(yield 3);
}
const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
console.log(gen.return('end')); // { done: false, value: 'end' }
console.log(gen.next()); // { done: true, value: undefined }
throw
函数也可以中断迭代,同时抛出一个错误,throw
函数接受一个参数作为错误原因:
javascript
function * fn () {
yield 1;
yield 2;
}
const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
gen.throw('error: stop'); // Uncaught error: stop
如果在生成器中使用try...catch...
捕获了这个错误,则迭代不会中断,而是跳过当前yield(实际在chrome中,迭代被直接结束了):
javascript
function * fn () {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log(e);
}
}
const gen = fn();
// 以下为chrome中执行结果,与红宝书描述不一致
console.log(gen.next()); // { done: false, value: 1 }
gen.throw('error: stop'); // error: stop
console.log(gen.next()); // { done: true, value: undefined }
console.log(gen.next()); // { done: true, value: undefined }
如果生成器还未开始执行就调用了throw
方法,则相当于在函数外部抛出错误,会中断代码执行:
javascript
function * fn () {
try {
yield 1;
yield 2;
yield 3;
} catch (e) {
console.log(e);
}
}
const gen = fn();
// 以下为chrome中执行结果
gen.throw('error: stop'); // 代码执行被中断,并抛出错误:Uncaught error: stop
// 下面代码不会执行
console.log(gen.next());
console.log(gen.next());
生成器中还可以使用yield *
直接消费一个可迭代对象:
javascript
function * fn () {
yield * [1, 2, 3];
// 等价于
// yield 1;
// yield 2;
// yield 3;
}
for (const v of fn()) {
console.log(v); // 依次执行1, 2, 3
}
5. 使用生成器
生成器最大的作用在于可以中断代码执行,利用这个特性可以实现类似async await
的效果。事实上来说,async await
就是一个底层语法糖,内部实现原理就是利用了生成器。可以查看co这个库了解生成器的使用。
javascript
import co from 'co';
co(function *(){
// yield any promise
var result = yield Promise.resolve(true);
}).catch(onerror);
co(function *(){
// resolve multiple promises in parallel
var a = Promise.resolve(1);
var b = Promise.resolve(2);
var c = Promise.resolve(3);
var res = yield [a, b, c];
console.log(res);
// => [1, 2, 3]
}).catch(onerror);