
本文是 重温基础 系列文章的第十三篇。
今日感受:每次自我年终总结,都会有各种情绪和收获。
本章节复习的是JS中的迭代器和生成器,常常用来处理集合。
前置知识:
JavaScrip已经提供多个迭代集合的方法,从简单的 for循环到 map()和 filter()。
迭代器和生成器将迭代的概念直接带入核心语言,并提供一种机制来自定义 for...of循环的行为。
本文会将知识点分为两大部分,简单介绍和详细介绍:
简单介绍,适合基础入门会使用的目标;
详细介绍,会更加深入的做介绍,适合理解原理;
1. 概述
当我们使用循环语句迭代数据时,需初始化一个变量来记录每一次迭代在数据集合中的位置:
-
let a = ["aaa","bbb","ccc"]; -
for (let i = 0; i< a.length; i++){ -
console.log(a[i]); -
}
这边的 i就是我们用来记录迭代位置的变量,但是在ES6开始,JavaScrip引入了迭代器这个特性,并且新的数组方法和新的集合类型(如 Set集合与 Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的 for-of循环、展开运算符( ...),甚至连异步编程都可以使用迭代器。
本文主要会介绍ES6中新增的迭代器(Iterator)和生成器(Generator)。
2. 迭代器(简单介绍)
迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个 next()方法,每次调用都会返回一个结果对象。
这个结果对象,有两个属性:
-
value: 表示下一个将要返回的值。 -
done: 一个布尔值,若没有更多可返回的数据时,值为 true,否则 false。
如果最后一个值返回后,再调用 next(),则返回的对象的 done值为 true,而 value值如果没有值的话,返回的为 undefined。
ES5实现一个迭代器:
-
function myIterator(list){ -
var i = 0; -
return { -
next: function(){ -
var done = i >= list.length; -
var value = !done ? list[i++] : undefined; -
return { -
done : done, -
value : value -
} -
} -
} -
} -
var iterator = myIterator([1,2,3]); -
iterator.next(); // "{done: false, value: 1}" -
iterator.next(); // "{done: false, value: 2}" -
iterator.next(); // "{done: false, value: 3}" -
iterator.next(); // "{done: true, value: undefined}" -
// 以后的调用都一样 -
iterator.next(); // "{done: true, value: undefined}"
从上面代码可以看出,ES5的实现还是比较麻烦,而ES6新增的生成器,可以使得创建迭代器对象的过程更加简单。
3. 生成器(简单介绍)
生成器是一种返回迭代器的函数,通过 function关键字后的星号( *)来表示,函数中会用到新的关键字 yield。星号可以紧挨着 function关键字,也可以在中间添加一个空格。
-
function *myIterator(){ -
yield 1; -
yield 2; -
yield 3; -
} -
let iterator = myIterator(); -
iterator.next(); // "{done: false, value: 1}" -
iterator.next(); // "{done: false, value: 2}" -
iterator.next(); // "{done: false, value: 3}" -
iterator.next(); // "{done: true, value: undefined}" -
// 以后的调用都一样 -
iterator.next(); // "{done: true, value: undefined}"
生成器函数最有趣的部分是,每当执行完一条 yield语句后函数就会自动停止执行,比如上面代码,当 yield1;执行完后,便不会执行任何语句,而是等到再调用迭代器的 next()方法才会执行下一个语句,即 yield2;.
使用 yield关键字可以返回任何值和表达式,因为可以通过生成器函数批量给迭代器添加元素:
-
function *myIterator(list){ -
for(let i = 0; i< list.length ; i ++){ -
yield list[i]; -
} -
} -
var iterator = myIterator([1,2,3]); -
iterator.next(); // "{done: false, value: 1}" -
iterator.next(); // "{done: false, value: 2}" -
iterator.next(); // "{done: false, value: 3}" -
iterator.next(); // "{done: true, value: undefined}" -
// 以后的调用都一样 -
iterator.next(); // "{done: true, value: undefined}"
生成器的适用返回很广,可以将它用于所有支持函数使用的地方。
4. 迭代器(详细介绍)
4.1 Iterator迭代器概念
Iterator是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成迭代操作(即依次处理该数据结构的所有成员)。
Iterator三个作用:
- 为各种数据结构,提供一个统一的、简便的访问接口;
- 使得数据结构的成员能够按某种次序排列;
- Iterator 接口主要供ES6新增的
for...of消费;
4.2 Iterator迭代过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,迭代器对象本质上,就是一个指针对象。
- 第一次调用指针对象的
next方法,可以将指针指向数据结构的第一个成员。 - 第二次调用指针对象的
next方法,指针就指向数据结构的第二个成员。 - 不断调用指针对象的
next方法,直到它指向数据结构的结束位置。
每一次调用 next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含 value和 done两个属性的对象。
-
value属性是当前成员的值; -
done属性是一个布尔值,表示迭代是否结束;
模拟 next方法返回值:
-
let f = function (arr){ -
var nextIndex = 0; -
return { -
next:function(){ -
return nextIndex < arr.length ? -
{value: arr[nextIndex++], done: false}: -
{value: undefined, done: true} -
} -
} -
} -
let a = f(['a', 'b']); -
a.next(); // { value: "a", done: false } -
a.next(); // { value: "b", done: false } -
a.next(); // { value: undefined, done: true }
4.3 默认Iterator接口
若数据可迭代,即一种数据部署了Iterator接口。
ES6中默认的Iterator接口部署在数据结构的 Symbol.iterator属性,即如果一个数据结构具有 Symbol.iterator属性,就可以认为是可迭代。
Symbol.iterator属性本身是函数,是当前数据结构默认的迭代器生成函数。执行这个函数,就会返回一个迭代器。至于属性名 Symbol.iterator,它是一个表达式,返回 Symbol对象的 iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。
原生具有Iterator接口的数据结构有:
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
4.4 Iterator使用场景
- (1)解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用 Symbol.iterator方法。
-
let a = new Set().add('a').add('b').add('c'); -
let [x, y] = a; // x = 'a' y = 'b' -
let [a1, ...a2] = a; // a1 = 'a' a2 = ['b','c']
- (2)扩展运算符
扩展运算符( ...)也会调用默认的 Iterator 接口。
-
let a = 'hello'; -
[...a]; // ['h','e','l','l','o'] -
let a = ['b', 'c']; -
['a', ...a, 'd']; // ['a', 'b', 'c', 'd']
- (2)yield*
yield*后面跟的是一个可迭代的结构,它会调用该结构的迭代器接口。
-
let a = function*(){ -
yield 1; -
yield* [2,3,4]; -
yield 5; -
} -
let b = a(); -
b.next() // { value: 1, done: false } -
b.next() // { value: 2, done: false } -
b.next() // { value: 3, done: false } -
b.next() // { value: 4, done: false } -
b.next() // { value: 5, done: false } -
b.next() // { value: undefined, done: true }
- (4)其他场合
由于数组的迭代会调用迭代器接口,所以任何接受数组作为参数的场合,其实都调用了迭代器接口。下面是一些例子。 - for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
newMap([['a',1],['b',2]])) - Promise.all()
- Promise.race()
4.5 for...of循环
只要数据结构部署了 Symbol.iterator属性,即具有 iterator 接口,可以用 for...of循环迭代它的成员。也就是说, for...of循环内部调用的是数据结构的 Symbol.iterato方法。
使用场景:
for...of可以使用在数组, Set和 Map结构,类数组对象,Genetator对象和字符串。
- 数组
for...of循环可以代替数组实例的 forEach方法。
-
let a = ['a', 'b', 'c']; -
for (let k of a){console.log(k)}; // a b c -
a.forEach((ele, index)=>{ -
console.log(ele); // a b c -
console.log(index); // 0 1 2 -
})
与 for...in对比, for...in只能获取对象键名,不能直接获取键值,而 for...of允许直接获取键值。
-
let a = ['a', 'b', 'c']; -
for (let k of a){console.log(k)}; // a b c -
for (let k in a){console.log(k)}; // 0 1 2
- Set和Map
可以使用数组作为变量,如 for(let[k,v]of b){...}。
-
let a = new Set(['a', 'b', 'c']); -
for (let k of a){console.log(k)}; // a b c -
let b = new Map(); -
b.set('name','leo'); -
b.set('age', 18); -
b.set('aaa','bbb'); -
for (let [k,v] of b){console.log(k + ":" + v)}; -
// name:leo -
// age:18 -
// aaa:bbb
- 类数组对象
-
// 字符串 -
let a = 'hello'; -
for (let k of a ){console.log(k)}; // h e l l o -
// DOM NodeList对象 -
let b = document.querySelectorAll('p'); -
for (let k of b ){ -
k.classList.add('test'); -
} -
// arguments对象 -
function f(){ -
for (let k of arguments){ -
console.log(k); -
} -
} -
f('a','b'); // a b
- 对象
普通对象不能直接使用 for...of会报错,要部署Iterator才能使用。
-
let a = {a:'aa',b:'bb',c:'cc'}; -
for (let k in a){console.log(k)}; // a b c -
for (let k of a){console>log(k)}; // TypeError
4.6 跳出for...of
使用 break来实现。
-
for (let k of a){ -
if(k>100) -
break; -
console.log(k); -
}
5. 生成器(详细介绍)
5.1 基本概念
Generator生成器函数是一种异步编程解决方案。
原理:
执行 Genenrator函数会返回一个遍历器对象,依次遍历 Generator函数内部的每一个状态。
Generator函数是一个普通函数,有以下两个特征:
-
function关键字与函数名之间有个星号; - 函数体内使用
yield表达式,定义不同状态;
通过调用 next方法,将指针移向下一个状态,直到遇到下一个 yield表达式(或 return语句)为止。简单理解, Generator函数分段执行, yield表达式是暂停执行的标记,而 next恢复执行。
-
function * f (){ -
yield 'hi'; -
yield 'leo'; -
return 'ending'; -
} -
let a = f(); -
a.next(); // {value: 'hi', done : false} -
a.next(); // {value: 'leo', done : false} -
a.next(); // {value: 'ending', done : true} -
a.next(); // {value: undefined, done : false}
5.2 yield表达式
yield表达式是暂停标志,遍历器对象的 next方法的运行逻辑如下:
- 遇到
yield就暂停执行,将这个 yield后的表达式的值,作为返回对象的 value属性值。 - 下次调用
next往下执行,直到遇到下一个 yield。 - 直到函数结束或者
return为止,并返回 return语句后面表达式的值,作为返回对象的 value属性值。 - 如果该函数没有
return语句,则返回对象的 value为 undefined 。
注意:
-
yield只能用在 Generator函数里使用,其他地方使用会报错。
-
// 错误1 -
(function(){ -
yiled 1; // SyntaxError: Unexpected number -
})() -
// 错误2 forEach参数是个普通函数 -
let a = [1, [[2, 3], 4], [5, 6]]; -
let f = function * (i){ -
i.forEach(function(m){ -
if(typeof m !== 'number'){ -
yield * f (m); -
}else{ -
yield m; -
} -
}) -
} -
for (let k of f(a)){ -
console.log(k) -
}
-
yield表达式如果用于另一个表达式之中,必须放在圆括号内。
-
function * a (){ -
console.log('a' + yield); // SyntaxErro -
console.log('a' + yield 123); // SyntaxErro -
console.log('a' + (yield)); // ok -
console.log('a' + (yield 123)); // ok -
}
-
yield表达式用做函数参数或放在表达式右边,可以不加括号。
-
function * a (){ -
f(yield 'a', yield 'b'); // ok -
lei i = yield; // ok -
}
5.3 next方法
yield本身没有返回值,或者是总返回 undefined, next方法可带一个参数,作为上一个 yield表达式的返回值。
-
function * f (){ -
for (let k = 0; true; k++){ -
let a = yield k; -
if(a){k = -1}; -
} -
} -
let g =f(); -
g.next(); // {value: 0, done: false} -
g.next(); // {value: 1, done: false} -
g.next(true); // {value: 0, done: false}
这一特点,可以让 Generator函数开始执行之后,可以从外部向内部注入不同值,从而调整函数行为。
-
function * f(x){ -
let y = 2 * (yield (x+1)); -
let z = yield (y/3); -
return (x + y + z); -
} -
let a = f(5); -
a.next(); // {value : 6 ,done : false} -
a.next(); // {value : NaN ,done : false} -
a.next(); // {value : NaN ,done : true} -
// NaN因为yeild返回的是对象 和数字计算会NaN -
let b = f(5); -
b.next(); // {value : 6 ,done : false} -
b.next(12); // {value : 8 ,done : false} -
b.next(13); // {value : 42 ,done : false} -
// x 5 y 24 z 13
5.4 for...of循环
for...of循环会自动遍历,不用调用 next方法,需要注意的是, for...of遇到 next返回值的 done属性为 true就会终止, return返回的不包括在 for...of循环中。
-
function * f(){ -
yield 1; -
yield 2; -
yield 3; -
yield 4; -
return 5; -
} -
for (let k of f()){ -
console.log(k); -
} -
// 1 2 3 4 没有 5
5.5 Generator.prototype.throw()
throw方法用来向函数外抛出错误,并且在Generator函数体内捕获。
-
let f = function * (){ -
try { yield } -
catch (e) { console.log('内部捕获', e) } -
} -
let a = f(); -
a.next(); -
try{ -
a.throw('a'); -
a.throw('b'); -
}catch(e){ -
console.log('外部捕获',e); -
} -
// 内部捕获 a -
// 外部捕获 b
5.6 Generator.prototype.return()
return方法用来返回给定的值,并结束遍历Generator函数,如果 return方法没有参数,则返回值的 value属性为 undefined。
-
function * f(){ -
yield 1; -
yield 2; -
yield 3; -
} -
let g = f(); -
g.next(); // {value : 1, done : false} -
g.return('leo'); // {value : 'leo', done " true} -
g.next(); // {value : undefined, done : true}
5.7 next()/throw()/return()共同点
相同点就是都是用来恢复Generator函数的执行,并且使用不同语句替换 yield表达式。
-
next()将 yield表达式替换成一个值。
-
let f = function * (x,y){ -
let r = yield x + y; -
return r; -
} -
let g = f(1, 2); -
g.next(); // {value : 3, done : false} -
g.next(1); // {value : 1, done : true} -
// 相当于把 let r = yield x + y; -
// 替换成 let r = 1;
-
throw()将 yield表达式替换成一个 throw语句。
-
g.throw(new Error('报错')); // Uncaught Error:报错 -
// 相当于将 let r = yield x + y -
// 替换成 let r = throw(new Error('报错'));
-
next()将 yield表达式替换成一个 return语句。
-
g.return(2); // {value: 2, done: true} -
// 相当于将 let r = yield x + y -
// 替换成 let r = return 2;
5.8 yield* 表达式
用于在一个Generator中执行另一个Generator函数,如果没有使用 yield*会没有效果。
-
function * a(){ -
yield 1; -
yield 2; -
} -
function * b(){ -
yield 3; -
yield * a(); -
yield 4; -
} -
// 等同于 -
function * b(){ -
yield 3; -
yield 1; -
yield 2; -
yield 4; -
} -
for(let k of b()){console.log(k)} -
// 3 -
// 1 -
// 2 -
// 4
5.9 应用场景
- 控制流管理
解决回调地狱: -
// 使用前 -
f1(function(v1){ -
f2(function(v2){ -
f3(function(v3){ -
// ... more and more -
}) -
}) -
}) -
// 使用Promise -
Promise.resolve(f1) -
.then(f2) -
.then(f3) -
.then(function(v4){ -
// ... -
},function (err){ -
// ... -
}).done(); -
// 使用Generator -
function * f (v1){ -
try{ -
let v2 = yield f1(v1); -
let v3 = yield f1(v2); -
let v4 = yield f1(v3); -
// ... -
}catch(err){ -
// console.log(err) -
} -
} -
function g (task){ -
let obj = task.next(task.value); -
// 如果Generator函数未结束,就继续调用 -
if(!obj.done){ -
task.value = obj.value; -
g(task); -
} -
} -
g( f(initValue) ); - 异步编程的使用 在真实的异步任务封装的情况:
-
let fetch = require('node-fetch'); -
function * f(){ -
let url = 'http://www.baidu.com'; -
let res = yield fetch(url); -
console.log(res.bio); -
} -
// 执行该函数 -
let g = f(); -
let result = g.next(); -
// 由于fetch返回的是Promise对象,所以用then -
result.value.then(function(data){ -
return data.json(); -
}).then(function(data){ -
g.next(data); -
})
参考资料
1.MDN 迭代器和生成器
2.ES6中的迭代器(Iterator)和生成器(Generator)











