本文将以我自己的理解方式,说说for..of到底是个啥(顺便地讲迭代器)。
本人太烂,有错误敬请指出。
引言
从对Array的使用中可以发现,for..of类似如下:
Array.prototype.abcd = function(){};
var a = ['foo', 'bar'];
for(let i in a) {
if(a.hasOwnProperty(i)) {
let b = a[i];
console.log(b);
}
}
/* 能实现如下?
for(let b of a) {
console.log(b);
}
*/小提示:
如果不加这个判断,abcd会显示,但Array.prototype.find等原型不会。为何?
这是另一个话题:可枚举,可延伸到“属性描述符”,与本文主题关系很小,就不展开讲了。
这么着,似乎就是个语法糖罢了?
试试看:
上面那段程序,输入普通对象 ({k:v,...}Plain Object),for..of的直接报错,for..in的看上去正常;
如果输入jQuery对象 ($(sth)),for..in的多了个length,for..of的正常。
可以发现,
for..of和for..in并不类似(除了语法上类似)。
事实上,for..of要求对象实现一些方法,普通对象没有实现,自然没法使用。Array等对象(包括jQuery)实现好了,就能用。
哪些方法呢?见下。
可迭代
上文说,for..of要求对象实现一些方法。
那么,定义:
- 实现了这些方法的对象 可迭代。
- 这些要求为 可迭代协议。
那到底要求什么?
要求可迭代对象的
Symbol.iterator属性(原型链上也行)为函数,
不带参数调用时返回一个的迭代器对象,用于迭代this。
实例如下,a实现了可迭代协议,因此a可迭代。
var a = { foo: 22, bar: 33 };
a[Symbol.iterator] = function () {
return [0,1,2][Symbol.iterator]();
//要在函数中调用,直接把函数赋值会不输出!为什么?
//因为那样this指向a而非数组,遍历的也就是a了。也可以用bind改变this。
}
/*
现在用for..of遍历a,能遍历了!
会输出0 1 2,因为把数组的迭代器搬来了。
*/Symbol.iterator是什么?Symbol类型的值可以作为键名,Symbol.iterator是一个Symbol类型的值。
迭代器对象是什么?见下。
迭代器
迭代器是一个对象(任何对象都行,包括Proxy之类)。
另外:
使用迭代器过程中,可以不传入任何参数。(
for..of是不传入的,但迭代器本身没有规定不能传入)
- 被迭代器迭代的对象,在迭代器创建时已经定好,通常由闭包传递
Symbol.iterator的this来保存。- 对于当前迭代到的位置索引,可以通过闭包保存,也可以通过迭代器对象的属性。
迭代器的要求如下:
有一个
next方法,用于进行下一次迭代。next方法返回一个对象,属性如下:
键名 类型 表示 done 布尔 如果为真,则表示迭代已经结束,没有值,value为默认值或未定义 value 任意 如果done为假,则为本次迭代获得的值
是不是很懵逼?不知道到底干啥?或者大概明白不知道有啥用?
看看下面一例。
使用迭代器
var iterable = [1, 4, 5, 9, 0, 8];
var iterator = iterable[Symbol.iterator]();
var valObj;
while(1) {
valObj = iterator.next();
if(valObj.done) break;
console.log(valObj.value);
}会迭代iterator(为迭代器),输出每一项。
其是从iterable(为可迭代对象)获取到的迭代器。
手写一个
写个对象,使其可迭代。
var iterable = {
[Symbol.iterator]() {
var vals = Object.values(this);
var index = 0;
return {
next() {
return {
value: vals[index++],
done: index > vals.length
}
}
};
}
}
iterable.a = 111;
iterable.b = 222;
iterable.hhhh = "ABC";
for(let v of iterable) {
console.log(v);
}使用了语法:计算属性名
现在对象iterable就可以像最开始那一例一样输出对象的值了。
将其放在Object.prototype上,就能用for..of迭代全部对象,像我们期望的那样!
(不过不建议修补原生对象)
其实已经结束了。。。但是还有点东西,继续讲吧:
但是这么来,本来能直接用循环等方法解决的,现在却很困难。这还不如类似forEach的玩意,不是捡了芝麻、丢了西瓜吗?
别急,有一种更好的方法来制作迭代器。
生成器
生成器定义一个函数 (生成器函数)。
一般用function*代替function获得生成器对象,如function* a(){}
对象字面量可以在方法前跟*,如{*a(){}}。来源
调用生成器函数时,不会执行函数体内容,而是返回一个对象。
该对象既是迭代器,又可迭代,其Symbol.iterator方法返回它自己。
函数体将被异步地挂起,直到迭代器调用了next方法。
生成器函数内,可用一个新的表达式:yield,语法类似void(也就是yield 1和yield(1)都行)。
当next被调用时,如果函数未结束(已结束的情况后面讲),
生成器函数就会从上一次停下的地方开始(如果没运行过,就是从头开始)继续运行,
直到遇到yield或return后,生成器函数停下。
怎么个停下法?
把
yield "sth"想象成调用一个“函数”,却没有函数体,而是其他的(调用next后直到下一次调用next)代码内容。
而“函数”返回时,就是下一次调用next时。如果知道C,那可以把它想象成
longjmp;如果用过js的async函数,那更是异曲同工。
生成器函数停下后,next函数返回。返回值是:
{
done: false,
value: //yield后跟的值
}如果函数已经结束了(比如运行到底了,或者返回过了),那返回值是:
{
done: true,
value: undefined
}举例。
写生成器
var iterable = {
*[Symbol.iterator]() {
for(let i in this) {
yield this[i];
}
}
}
Object.assign(iterable, { a: 111, b: 222 });
for(let v of iterable) {
console.log(v);
}和上面的功能一样,但简单多了,for..in使其很容易理解。
return与yield*
生成器函数内,还可使用return与yield*。
return与yield有点类似,但与本来的功能也差不多。
作用就是直接结束这个迭代器(后面语句不执行了,和原本功能一样),返回next函数,在返回值的value可选地放一个值。
遇到return而非yield,则next函数的返回值为:
{
done: true,
value: //return后跟的值
}yield*则是直接使用另一个可迭代对象的迭代器。
如下两个相当(也许不同,望指正):
yield* sth;for(let v of sth) {
yield v;
}因此那一例还可以写作:
var iterable = {
*[Symbol.iterator]() {
yield* Object.values(this);
}
}更简单了。。。
传入参数
这部分和for..of无关,因此粗略带过。
生成器函数可以传入参数。
使用迭代器的代码从“使用迭代器”一节借来,因为for..of无法传入参数。
var generator = function*(p) { //有一个参数
yield* p;
};
var iterator = generator([1, 4, 9]); //传入参数
var valObj;
while(1) {
valObj = iterator.next();
if(valObj.done) break;
console.log(valObj.value);
}生成器函数生成的迭代器的
next函数可以传入参数。
传入的参数将被作为上一个yield语句的值。
还记得前面那个“函数”的比喻吗?next方法传入的参数就是yield“函数”的“返回值”。
也可以将yield想象成prompt函数,生成器函数想象成程序,使用迭代器者就是你。
第一次调用next,程序开始运行,你等待;
程序遇到yield,告知你值。程序阻塞;
你拿值进行计算完毕,准备再次调用next,可以输入一个值(next方法的参数);
程序用prompt的返回值(yield语句的值)收到你的输入,继续运行。
如果还是不懂,那也没必要懂了,因为和for..of没啥关系,呵呵。