在写Symbol类型的时候碰到了[Symbol.iterator]的属性,在了解[Symbol.iterator]的过程中,发现对迭代器,for in,for of和generator的概念了解也不甚清晰,于是单开一篇学习这个问题。 提前思考的问题:
- for...in和for...of有什么不同?
- 什么是generator生成器?
Iterator
js中表示集合的数据类型:
- Array
- Object
- Set(es6新增)
- Map(es6新增)
what
Iterator是一种接口,为不同的数据结构提供统一的访问机制,任何数据结构只要有Iterator,就可以依次访问该数据结构内所有的成员。
why
将遍历方法抽象出来,避免暴露数据结构内部,有的需要for(i<index),有的如同链表需要while(e.next!==null)。降低遍历的方法和数据结构本身的耦合程度。
Iterator本身也是一种设计模式,称为迭代器模式:不关心被迭代对象的内部构造,仅提供一种方法顺序访问聚合对象中的各个元素。
可以引申为对于想实现相同目的不同的方法,都可以依据优先级放入迭代器中,迭代器从头开始访问每个方法,直到遇到return不为false的为止,即将传统迭代中作为指针的i变成具体的方法。
how
实现一个js的Iterator:
- 创建一个指针对象,指向起始位置,它具有
next()方法 - 不断调用指针对象的
next()方法,指针指向数据结构的第n个成员。 - 指针指向了数据结构的末尾,迭代结束
具体实现牢记三个对象:可迭代对象,迭代器对象,迭代结果对象
(可迭代对象)通过Iterator(迭代器对象)返回一个对象(迭代结果对象),对象包含next()方法,next()方法返回一个包含value和done的值的对象。
function makeIterator(arr) {
let index = 0;
return {
next: function() {
return index < arr.length ? {
value: arr[index ++],
} : {
done: true,
}
}
};
}
for...of
既然已经有了默认的Iterator方法,提供迭代器对象,那自然也应该有一个方法去调用这个迭代器对象。
for...of是es6提供的一种迭代器消费方式,它会去自动寻找Iterator接口,也就是数据结构的[Symbol.iterator]属性。有些数据结构原型就具有[Symbol.iterator]属性,有些没有。比如Object没有[Symbol.Iterator],使用for..of会报错TypeError;
for..of在不同数据类型下的运行结果:
// 数组类型,for of遍历访问数组的值
const arrayTest = [
1, 2, 'abc', null, undefined, 5,
{ name: 'jack', age: 20 },
{ name: 'tom', age: 18 },
[1, 2, 3],
];
for (let result of arrayTest) {
console.log(result);
}
// 字符串类型,输出每一个字符
const strTest = 'afgeggweg';
for (let result of strTest) {
console.log(result);
}
// Map类型
const mapTest = new Map();
mapTest.set("today", "tuesday");
mapTest.set("tomorrow", "friday");
for (let result of mapTest) {
console.log(result);
}
但是,当我们想对对象使用for...of时,就报错了。
const obj = {
a: '1',
b: '2',
};
for (let result of obj) {
console.log(result);
}
如果想要让对象可以使用for...of,需要给它手动定义一个[Symbol.iterator]
const obj = {
m: 'm',
1: 2,
a: 'a',
d: [1, 2],
[Symbol.iterator]() {
let index = 0;
let self = this;
return {
next() {
let keys = Object.keys(self);
if (index >= keys.length) {
return {
value: undefined,
done: true,
}
} else {
return {
value: self[keys[index ++]],
done: false,
}
}
}
}
}
};
for (let val of obj) {
console.log(val);
}
除此之外,for...of还有一些特性:
- 惰性加载,等到实际需要下一个值时才发生计算
- 可以通过return()在done还不为true时,跳出循环
generator生成器
个人平时开发中并没有较熟练地使用生成器,所以这里也想好好梳理一下生成器以及yield关键字的理解和使用。
每次调用iterator的next()会返回按照迭代规则,下一个应该输出的内容。但是当被迭代的不是数据结构,而是某些复杂的运算,可以使用生成器。想使用生成器,就在function和()之间加一个*
- 当有一个生成器函数function* test() { ... },调用它并不会执行这个函数,而是会返回一个生成器对象,这个生成器对象是一个迭代器,拥有next()方法,调用next()方法,它会开始执行直到遇到一个yield或return
- 在使用next()时可以往里添加参数,它会作为上一个yield表达式的值,如果不传相当于传递了一个undefined
- next()返回的结果是一个迭代结果对象{ value: .., done: .. }
- 当试图在一个generator中再使用另一个generator时,需要用yield* 来定义它,相当于for...of,因为yield* 可以用来迭代任何可迭代对象
- yield和yield* 只能在生成器函数中使用
function* A() {
yield 'a1';
yield 'a2';
yield 'a3';
}
function* B() {
yield 'b1';
yield 'b2';
yield A();
yield 'b3';
}
let a = B();
// 输出结果,当使用的是yield时,无法迭代A(),返回一个generator对象
console.log(a.next()); // { value: 'b1', done: false }
console.log(a.next()); // { value: 'b2', done: false }
console.log(a.next()); // { value: Object [Generator] {}, done: false }
console.log(a.next()); // { value: 'b3', done: false }
function* C() {
yield 'c1';
yield* A();
yield 'c2';
}
let a = C();
// 输出结果,使用的是yield*,相当于迭代了A
console.log(a.next()); // { value: 'c1', done: false }
console.log(a.next()); // { value: 'a1', done: false }
console.log(a.next()); // { value: 'a2', done: false }
console.log(a.next()); // { value: 'a3', done: false }
console.log(a.next()); // { value: 'c2', done: false }
yield意为回传,当调用next时,生成器会运行到yield表达式,yield关键字后面的表达式会被求值,作为next()调用的返回值(yield将结果传给调用者),而调用者通过next()可以向生成器传值(next将入参传给生成器),可以形成交替控制。
生成器的return()和throw()
在生成器上调用内置的return和throw方法,会改变生成器的控制流,就像生成器的下一条语句是return和throw一样
通过在生成器函数中定义try{...}finally{...},可以使用return来保证使用finally中的语句,且生成器再也不会被使用。
而使用throw(),也可以像next一样传给生成器一个错误信息,告诉它应该通过这个异常来改变控制流。
生成器是非常强大的控制结构,可以配合新增的async和await关键字使用(之后讨论),将虽然是单线程的javascript实现异步管理,代码看起来依旧像是同步的。
for...in
最后讨论一下开头的for..of(es6)和for...in(es5)问题,我们已经知道了for...of其实是使用被迭代对象的[Symbol.iterator]迭代器对象,那for...in是做什么的呢?如何使用呢?为什么会被使用呢?
for...in是用来遍历数字或者对象的属性,得到和了for...of的第一个区别:获取被遍历的属性,而非值,且可以对对象使用
// 简单粗暴的实验
const test = [
1,2,3,4
];
for (let value of test) {
console.log(value); // 1,2,3,4 得到了数组的值
}
for (let result in test) {
console.log(result); // 0,1,2,3 输出的是对象的下标
}
// 如果被遍历对象是字符串,会输出每个字符在字符串中的位置
第二个区别:for...in可以获取被遍历对象中的自定义属性
const test = [1, 2, 3];
test.name = 'jack';
for (let value of test) {
console.log(value); // 1 2 3
}
for (let result in test) {
console.log(result); // 只有for...in才会输出jack
}
第三个区别:for...in返回的索引都是字符串类型,而for...of输出的结果是原类型
const test = [1, 2, 'name', true];
test.name = 'jack';
for (let value of test) {
console.log(value, typeof value);
}
for (let result in test) {
console.log(result, typeof result);
}
PS: 所以不要用for...in遍历数组,得到的下标也不能直接计算
第四个区别:for...in会跳过空值的属性(空值不是undefined和null)
const test = [1, 2, 'name',, true]; // 有一个空值,如果为undefined和null就不是空值
for (let value of test) {
console.log(value, typeof value);
}
for (let result in test) {
console.log(result, typeof result);
}
建议使用for...in遍历对象,for...in和for...of都可以打断,但forEach不可以打断