概述
这是我看你不知道的JavaScript(中卷)的读书笔记,供以后开发时参考,相信对其他人也有用。
生成器和迭代器
生成器是另一个封装了时间的框架,它用function*来标识,执行生成器函数会返回一个迭代器,执行迭代器能控制生成器中代码的执行。下面是一个简单的实例:
var x = 1;
//这是一个生成器函数foo
function *foo() {
x++;
yield; // 暂停!
console.log( "x:", x );
}
function bar() {
x++;
}
// 构造一个迭代器it来控制这个生成器
var it = foo();
// 这里启动foo()!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3
it.next().value等于每次执行时yield后面表达式的值或者函数return后面的值,所以上面两次it.next().value都等于undefined。下面是一个加了表达式的示例:
function *foo(x) {
var y = x * (yield "Hello"); // <-- yield一个值!
return y;
}
var it = foo( 6 );
var res = it.next(); // 第一个next(),并不传入任何东西
res.value; // "Hello"
res = it.next( 7 ); // 向等待的yield传入7
res.value; // 42
需要注意的是,每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器控制的是这个生成器实例。
更多时候,如果我们不通过next函数传值的话,我们会对生成器进行封装。
var a = 1;
var b = 2;
function *foo() {
a++;
yield;
b = b * a;
a = (yield b) + 3;
}
function *bar() {
b--;
yield;
a = (yield 8) + b;
b = a * (yield 2);
}
//封装函数
function step(gen) {
var it = gen();
var last;
return function() {
// 不管yield出来的是什么,下一次都把它原样传回去!
last = it.next( last ).value;
};
}
// 确保重新设置a和b
a = 1;
b = 2;
var s1 = step( foo );
var s2 = step( bar );
// 首次运行*foo()
s1();
s1();
s1();
// 现在运行*bar()
s2();
s2();
s2();
s2();
console.log( a, b ); // 11 22
迭代器接口
除了执行生成器来建立一个迭代器外,我们还可以自己编写一个迭代器接口:
var something = (function(){
var nextVal;
return {
// for..of循环需要
[Symbol.iterator]: function(){ return this; },
// 标准迭代器接口方法
next: function(){
if (nextVal === undefined) {
nextVal = 1;
} else {
nextVal = (3 * nextVal) + 6;
}
return { done:false, value:nextVal };
}
};
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105
从上述代码可以看出,迭代器都有一个[Symbol.iterator]
方法,它返回自身。并且可以看出next方法的执行逻辑。
值得一提的是,es6为每个数组对象添加了[Symbol.iterator]
方法,使用它可以得到一个迭代器对象。示例如下:
var a = [1,3,5,7,9][Symbol.iterator]()
a.next().value; //1
每个iterable(可迭代)的对象都有这个[Symbol.iterator]
方法,利用这个方法可以返回一个包含自身的迭代器对象。
es6还新增了一个for..of循环,使用它可以自动迭代迭代器:
for (var v of something) {
console.log( v );
// 不要死循环!
if (v > 500) {
break;
}
}
// 1 9 33 105 321 969
另外,利用Object.keys(obj)可以返回对象的所有实例属性然后进行迭代,利用for..in则可以迭代对象的所有实例属性和原型属性。
迭代器的异步使用
首先让我们来看下面这段代码:
var data = ajax( "..url 1.." );
console.log( data );
很显然,这段代码是不能运行的,但是我们在执行异步操作的时候,脑中其实更偏向于理解上面这种形式的代码,那么怎么让上面这种形式的代码能够正常运行呢?可以用生成器迭代器改写成下面这个样子:
function* foo(url) {
ajax(url, function(data) {
yield data;
})
}
var data = foo(url);
console.log(data.next().value);
虽然我们期望这么运行,然而事实上上面这段代码根本不能运行,原因是yield只能在function*里面运行,不能在其它函数里面运行,否则会报错。所以只能退而求其次,改成如下代码:
function foo(url) {
ajax(url, function(data) {
it.next(data);
})
}
function* main(url) {
var text = yield foo(url);
console.log(text);
}
var it = main(url);
it.next();
上述代码还是很有技巧性的,看起来不是很容易理解,原因在于回调函数没有和ajax函数分离。所以用生成器+Promise的形式则容易理解得多:
function foo() {
return request(url);
}
function* main(url) {
var text = yield foo(url);
}
var it = main(url);
console.log(it.next().value);
虽然这里打印的是一个Promise对象,而不是返回的数据。但是整个过程看起来是同步的,实际执行却是异步的。其中的机制是,我们把异步过程封装在一个函数中,然后通过yield这个函数,使得我们能够在需要的时刻执行这个异步过程。
es5实现生成器
其实生成器并不是es6引入的新的东西,而是es5的一个polyfill。比如说下面这段生成器代码:
// request(..)是一个支持Promise的Ajax工具
function *foo(url) {
try {
console.log( "requesting:", url );
var val = yield request( url );
console.log( val );
}
catch (err) {
console.log( "Oops:", err );
return false;
}
}
var it = foo( "http://some.url.1" );
可以利用es5来实现这个生成器过程,代码如下。通过看这段代码,可以加深我们对生成器的理解。
function foo(url) {
// 管理生成器状态
var state;
// 生成器变量范围声明
var val;
function process(v) {
switch (state) {
case 1:
console.log( "requesting:", url );
return request( url );
case 2:
val = v;
console.log( val );
return;
case 3:
var err = v;
console.log( "Oops:", err );
return false;
}
}
// 构造并返回一个生成器
return {
next: function(v) {
// 初始状态
if (!state) {
state = 1;
return {
done: false,
value: process()
};
}
// yield成功恢复
else if (state == 1) {
state = 2;
return {
done: true,
value: process( v )
};
}
// 生成器已经完成
else {
return {
done: true,
value: undefined
};
}
},
"throw": function(e) {
// 唯一的显式错误处理在状态1
if (state == 1) {
state = 3;
return {
done: true,
value: process( e )
};
}
// 否则错误就不会处理,所以只把它抛回
else {
throw e;
}
}
};
}
可以看到,代码中用闭包封装了一个状态属性state和一个process方法,这个process方法根据不同的状态把原来的代码分块,最后通过构造返回一个生成器,来判断state的状态,然后根据process方法调用不同的代码块。