通过调用生成器得到的迭代器,暴露出一个next方法能让我们向迭代器请求一个新值。next方法返回一个携带着生成值的对象,而该对象中包含的另一个属性done也向我们指示了生成器是否还会追加生成值。
//通过调用生成器得到的迭代器,暴露出一个next方法能让我们向迭代器请求一个新值。next方法返回一个携带着生成值的对象,而该对象中包含的另一个属性done也向我们指示了生成器是否还会追加生成值。
console.log('-------使用while循环迭代生成器结果--------------');
function* WeaponGenerator() {
yield "Katana";
yield "Wakizashi";
}
//新建一个迭代器
const weaponGenerator = WeaponGenerator();
//创建一个变量,用这个变量来保存生成器产生的值
let item;
//每次循环都会从生成器中取出一个值,然后输出该值。当生成器不会再生成值的时候,停止迭代。
while (!(item = weaponGenerator.next()).done) {
if (item !== null) {
console.log("value:" + item.value);
}
}
通过生成器函数再次创建了一个迭代器对象:
const weaponGenerator = WeaponGenerator();
我们还创建了一个变量item,用于保存由生成器生成的单个值。随后,给While循环指定了条件:
while (!(item = weaponGenerator.next()).done) {
if (item !== null) {
console.log("value:" + item.value);
}
}
在每次迭代中,我们通过迭代器weaponGenerator的next方法从生成器中取一个值,然后把值存放在item变量中。和所有next返回的对象一样,item变量引用的对象中包含一个属性value为生成器返回的值,一个属性done指示生成器是否已经完成了值的生成。如果生成器中的值没有生成完毕,我们就会进入下次循环迭代,反之停止循环。
for-of循环不过是对迭代器进行迭代的语法糖:
for (let weapon of WeaponGenerator()) {
if (weapon !== undefined) {
console.log("weapon:" + weapon);
}
}
不同于手动调用迭代器的next方法,for-of循环同时还要查看生成器是否完成,它在后台自动做了完全相同的操作。
把执行权交给下一个生成器
正如在标准函数中调用另一个标准函数,我们需要把生成器的执行委托给另一个生成器。
console.log("------把执行权交给下一个生成器------");
console.log("---------------使用yield操作符将执行权交给另一个生成器--------------");
function* WarriorGenerator() {
yield 'Sun Tzu';
yield* NinjaGenerator(); //yield*将执行权交给另一个生成器
yield 'Genghis Kha';
}
function* NinjaGenerator() {
yield 'Hattori';
yield 'Yoshi';
}
for (let warrior of WarriorGenerator()) {
if (warrior !== null) {
console.log("warrior:" + warrior);
}
}
执行这段代码后会输出所有的字符串。第一个输出Sun Tzu不会有什么问题,因为它就是生成器WarriorGenerator得到的第一个值。
而对于第二个输出的值是Hattori需要解释一下:
在迭代器上使用yield*操作符,程序会跳转到另一个生成器上执行。在上述代码中,程序从WarriorGenerator跳转到一个新的NinjaGenerator生成器上,每次调用WarriorGenerator返回迭代器的next方法,都会执行重新寻址到了NinjaGenerator上。该生成器会一直持有执行权直到无工作可做。所以在上述代码中,生成Sun Tzu之后紧接着是Hattori和Yoshi。仅当NinjaGenerator的工作完成后,调用原来的迭代器才会继续输出Genghis Kha。注意,对于调用最初的迭代器代码来说,这一切都是透明的。for-of循环不会关心WarriorGenerator委托到另一个生成器上,它只关心在done状态到来之前一直调用next方法。
使用生成器
尝试生成唯一ID值。
console.log('-------------------------通过迭代器对象控制生成器--------------------------------');
//调用生成器函数不一定会执行生成器函数体。通过创建创建迭代器对象,可以与生成器通信。例如,可以通过迭代器对象请求满足条件的值。观察迭代器对象如何工作?
//定义一个生成器,它能生成一个包含两个weapon数据的序列
function* WeaponGenerator() {
yield 'Katana';
yield 'Wakizashi';
}
//调用生成器得到一个迭代器,从而我们能够控制生成器的执行
const weaponsIterator = WeaponGenerator();
//调用迭代器的next方法向生成器请求一个新值。
const result1 = weaponsIterator.next();
//结果为一个对象,其中包含着一个返回值,及一个指示器告诉我们生成器是否还会生成值
if (typeof result1 === 'object' && result1.value === 'Katana' && !result1.done) {
console.log('Katana received!');
}
//再次调用next方法从生成器中获取新值。
const result2 = weaponsIterator.next();
if (typeof result2 === 'object' && result2.value === 'Wakizashi' && !result2.done) {
console.log('Wakizashi received!');
}
//当没有可执行的代码,生成器就会返回'undefined'值,表示它的状态已经完成
const result3 = weaponsIterator.next();
if (typeof result3 === 'object' && result3.value === undefined && result3.done) {
console.log('There are no more results!');
}
在创建某些对象时,我们经常需要为每个对象赋一个唯一的ID值。最简单的方式是通过一个全局的计数器变量,但这是一种非常丑陋的写法,因为这个计数器变量很容易就会不慎淹没在混乱的代码中。另一种是使用生成器。
console.log("--------------------使用生成器生成ID序列----------------------");
//定义生成器函数IdGenerator
function* IdGenerator() {
//一个始终记录ID的变量,这个变量无法在生成器外部改变
let id = 0;
//循环生成无限长度的ID序列
while(true) {
yield ++id;
}
}
//这个迭代器,我们能够访问向生成器请求新的ID值。
const idIterator = IdGenerator();
//请求3个新ID值
const ninjaTest1 = { id:idIterator.next().value};
const ninjaTest2 = { id:idIterator.next().value};
const ninjaTest3 = { id:idIterator.next().value};
//测试运行结果
if (ninjaTest1.id === 1) {
console.log('First ninjaTest1 has id 1');
}
if (ninjaTest2.id === 2) {
console.log('Second ninjaTest2 has id 2');
}
if (ninjaTest3.id === 3) {
console.log('Third ninjaTest3 has id 3');
}
迭代器中包含一个局部变量id,其代表ID计数器。局部变量id仅能在生成器中被访问,故而完全不必要担心在代码的其他地方修改id值。随后是一个无限的while循环,其每次迭代都能生成一个新id值并挂起执行,直到下一次ID请求到达:
//定义生成器函数IdGenerator
function* IdGenerator() {
//一个始终记录ID的变量,这个变量无法在生成器外部改变
let id = 0;
//循环生成无限长度的ID序列
while(true) {
yield ++id;
}
}
注意:标准函数中一般不应该书写无限循环的代码。但在生成器中没问题!当生成器遇到一个yield语句,它就会一直挂起执行直到下次调用next方法,所以只有每次调用一次next方法,while循环才会迭代写一次并返回下一个ID值。
定义了生成器之后,又创建了一个迭代器对象:
const idIterator = IdGenerator();
我们能够调用idIterator.next()方法来控制生成器执行。每当遇到一次yield语句生成器就会停止执行,返回一个新的ID值可以给我们的对象赋值:
const ninjaTest1 = { id:idIterator.next().value};
看到这个方法是多么简单了吧?代码中没有任何会被不小心修改的全局变量。相反,我们使用迭代器从生成器中请求值。另外,如果还需要用另外一个迭代器来记录ID序列,我们只需要直接再初始化一个新迭代器就可以了。
使用迭代器遍历DOM树
网页的布局是基于DOM结构的,它是由HTML节点组成的树形结构,除了根节点的每个节点都只有一个父节点,而且可以有0个或多个孩子节点。由于DOM是网页开发中的基础,所以我们大部分代码都是围绕着对它的遍历。遍历DOM的相对简单的方式就是实现一个递归函数,在每次访问节点的时候都会被执行。
<div id="subTree">
<form>
<input type="text"/>
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script>
console.log('--------------------遍历递归DOM树---------------------');
function traverseDOM(element, callback) {
//用回调函数处理当前节点
callback(element);
element = element.firstElementChild;
//遍历每个子树
while (element) {
traverseDOM(element, callback);
element = element.nextElementSibling;
}
}
const subTree = document.getElementById("subTree");
//通过调用traverseDOM方法从根节点开始遍历
traverseDOM(subTree, function (element) {
if (element !== null) {
console.log("nodeName:" + element.nodeName);
}
});
</script>
使用递归函数函数来遍历id为subtree的所有节点,在访问每个节点的过程中我们还记录了该节点的类型。
<div id="subTree">
<form>
<input type="text"/>
</form>
<p>Paragraph</p>
<span>Span</span>
</div>
<script>
console.log('--------------------使用生成器遍历DOM树-----------------');
//用yield*将迭代控制转移到另一个DomTraveral生成器实例上。
function* DomTraversal(element) {
yield element;
element = element.firstElementChild;
while (element) {
yield* DomTraversal(element);
element = element.nextElementSibling;
}
}
const subTree = document.getElementById('subTree');
//使用for-of循环对节点进行循环迭代
for(let element of DomTraversal(subTree)) {
if (element !== null) {
console.log("nodeName:" + element.nodeName);
}
}
</script>
通过生成器实现DOM遍历,就像标准递一样简单,但它不必书写丑陋的回调函数代码。不同于在下一层递归处理每个访问过的节点子树,我们为每个访问过的节点创建一个生成器并将执行权交给它,从而使我们能够以迭代的方式书写概念上递归的代码。它的好处在于我们能够不凭借讨厌的回调函数,仅仅以一个简单的for-of循环就能处理生成的节点。
上述代码告诉我们如何在不必使用回调函数的情况下,使用生成器函数来解耦代码,从而将生产值(本例中是HTML节点)的代码和消费值(本例中的for-of循环打印、访问过的节点)的代码分隔开。除此以外,在很多场景下,使用迭代器要比使用递归都要自然,所以保持一个开放的思路很重要。
参考《JavaScript忍者秘籍》