重学JavaScript 篇的目的是回顾基础,方便学习框架和源码的时候可以快速定位知识点,查漏补缺,所有文章都同步在 公众号(道道里的前端栈) 和 github 上。
迭代器(Iterator)
一般在JavaScript里,for循环时最简单的迭代,比如:
for (let i = 0; i <= 10; i++) {
console.log(i);
}
循环是迭代机制的基础,这是因为它可以指定迭代的次数和每次迭代的操作。每次循环都会在下一次迭代开始前完成,并且迭代的顺序都是定好的。
迭代会在一个“有序”的集合上进行,有序的意思是有一定的顺序,不一定是自增或者自减,操作最多的就是数组了。最开始的时候,迭代过程中操作数组只能通过下标的方式获取,但是问题是如果不知道下标,就没法获取和操作值,所以后来增加了 Array.prototype.forEach 方法,里面可以直接获取到 项,算是for循环的递进版,但是仍然有问题的是,要想知道有没有遍历到最后一个,还是得通过下标。
上述只是数组,还有很多种类的集合多少都有自己的局限性,所以在ECMAScript6增加了 迭代器模式。
迭代器模式
迭代器模式表示可以把一些结构称之为 可迭代对象(Iterable),因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterable操作。
可迭代的数据类型最新典型的就是数组和类数组等集合对象,它们里面包含的元素是有限的,而且都具有无歧义的遍历顺序。
那可迭代对象到底好在哪儿呢?每个迭代器都有一个可迭代对象,迭代器会暴露对应的可迭代对象的API,这样的话迭代器无需了解可迭代对象内部结构,只需要知道如何取得连续的值!
迭代器的作用大致有三个:
- 访问数据
- 成员可排列
- 方便用
for...of操作
可迭代协议
实现可迭代协议(也就是Iterable接口)要求同时具备两个条件:
- 支持迭代
- 可创建实现Iterator接口的对象
也就是说,必须暴露出来一个属性作为“默认迭代器”,而且这个属性必须使用 Symbol.iterator作为键,并且它必须引用一个迭代器工厂函数,并且这个迭代器工厂函数返回一个新的迭代器。在工作过程中,并不需要显式的调用这个工厂函数来生成迭代器,因为实现了迭代协议的所有类型会自动兼容,以下是会调用Iterator接口的场景:
-
for...of
let arr = [1, 2, 3, 4, 5]; let iter = arr[Symbol.iterator](); for(let i of iter){ console.log(i) } //1 2 3 4 5 -
数组解构
let [a, b] = [1, 2, 3]; -
扩展操作符...
var str = 'hello'; [...str] // ['h','e','l','l','o'] -
Array.from()
-
Map和Set
let set = new Set().add('a').add('b').add('c'); let [x,y] = set; // x='a'; y='b' let [first, ...rest] = set; // first='a'; rest=['b','c']; -
Promise.all() 和 Promise.race()
-
yield*操作符
let generator = function* () { yield 1; yield* [2,3,4]; yield 5; }; var iterator = generator(); iterator.next() // { value: 1, done: false } iterator.next() // { value: 2, done: false } iterator.next() // { value: 3, done: false } iterator.next() // { value: 4, done: false } iterator.next() // { value: 5, done: false } iterator.next() // { value: undefined, done: true }
它们会在后台调用工厂函数,隐式创建一个迭代器。再简洁一点,凡是部署了Symbol.iterator属性的数据结构,就称为迭代器接口,凡是部署了迭代器接口的都可以用扩展运算符。
Iterator协议
Iterator使用 next() 方法遍历数据,每次成功调用next(),都会返回一个迭代器对象IteratorResult,其中包含了Iterator返回的下一个值。如果不调用next(),则无法知道迭代器的当前位置。
next()方法返回的迭代器对象 IteratorResult 包含2个属性:
- done:一个布尔值,表示是否还可以再次调用next()取得下一个值
- value:done为true时,value为undefined,done为false时,value为下一个可迭代对象的值
let arr = ["foo", "bar"];
// 迭代器
let iter = arr[Symbol.iterator]();
iter.next(); // {done: false, value: "foo"}
iter.next(); // {done: false, value: "bar"}
iter.next(); // {done: true, value: undefined}
如果可迭代对象被修改了,那么迭代器也会读取响应的改变:
let arr = ['foo', 'baz'];
let iter = arr[Symbol.iterator]();
iter.next()); // { done: false, value: 'foo' }
// 在数组中间插入值
arr.splice(1, 0, 'bar');
iter.next(); // { done: false, value: 'bar' }
iter.next(); // { done: false, value: 'baz' }
iter.next(); // { done: true, value: undefined }
终止迭代
如果想在迭代期间终止,可以通过 return() 方法中断退出。
return必须返回一个有效的IteratorResult对象,一般来说返回 {done: true},当然也可以有其他比较复杂的操作。
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代:
let arr = [1, 2, 3, 4, 5];
let iter = arr[Symbol.iterator]();
for (let i of iter){
console.log(i);
if(i > 2){
break;
}
}
// 1 2 3
for(let i of iter){
console.log(i)
}
// 4 5
使用
如果想遍历一个类数组,就必须往类数组上加一个Iterator接口,有一个简便方法,就是 Symbol.iterator 方法直接引用数组的 Iterator 接口。
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
生成器(Generator)
生成器是一个特别灵活的结构,它封装了多个内部状态,执行一个生成器会返回一个迭代器对象,所以生成器是一个包括了内部状态,返回迭代器对象的函数,也是就是说他可以自定义迭代器!
写法
生成器的形式是一个函数,函数名前加一个星号(*),表示它是一个生成器。哪里可以定义函数,哪里就可以定义生成器。
function* fn(){};
let fn = function* (){};
let obj = {
* fn()
}
function * fn(){}
//等价于
function* fn(){}
//等价于
function *fn(){}
注意:箭头函数不能用户来定义生成器,星号不受两侧空格的影响
当一个生成器函数被调用,就会产生一个生成器对象,生成器对象默认处于 暂停执行(suspended) 状态,生成器对象也带有Iterator接口,所以拥有next(),调用next后开始执行逻辑:
function* fn(){};
const gen = fn();
gen; // generatorFn (<suspended>)
gen.next; // f next(){ [native code] }
gen.next(); // { done: true, value: undefined }
生成器只会在第一次调用next方法后开始执行:
function *fn(){
console.log(123)
};
let gen = *fn();
gen.next(); // 123
yield 可以让生成器停止和开始执行。生成器函数遇到yield之前会正常执行,遇到之后开始暂停,函数作用于的状态会保留。要想继续执行,必须在生成器对象上调用next方法来恢复执行:
function *fn(){
yield;
yield "a";
return "b"
};
let gen = fn();
gen.next(); // {done: false, value: "a"}
gen.next(); // {done: true, value: "b"}
从上面可以看出来,yield生成的值会放在value里,yield退出的生成器函数的状态是false,return退出的生成器函数的状态是true。
所以:生成器函数被调用后,并不执行,返回值是一个指向内部状态的指针对象,也就是Iterator对象,必须调用next方法,将指针移向下一个状态。
yield
前面提到yield是管理生成器停止和开始的,这里把上面的例子详细说一下:
function *fn(){
yield;
yield "a";
return "b"
};
let gen = fn
gen.next(); // {done: false, value: undefined}
gen.next(); // {done: false, value: "a"}
gen.next(); // {done: true, value: "b"}
gen.next(); // {done: true, value: "undefined"}
- 调用第一次next方法,生成器方法开始执行,遇到yield表达式,暂停执行后面的操作,将紧跟yield的值作为value返回,没有值的话返回undefined,后面有其他值所以done是false
- 调用第二次next方法,继续执行,value是a,如果再次调用next还会输出其他值,所以done是false
- 调用第三次next方法,继续执行,value是b,如果再次调用next不会输出其他值了,所以done是true
- 调用第四次next方法,继续执行,value没有,所以是undefined,done是true
每次遇到field,函数都会暂停执行,下一次再从该位置出发。一个函数内部只能执行一次return,但是可以执行多次field表达式。正常函数只能返回一个值,生成器函数能返回多个值。
注意:yield只能在用在生成器函数中,用在其他地方会报错:
//1.
function* fn(){
function a(){
yield;
}
}
//2.
function* invalidGeneratorFnB() {
const b = () => {
yield;
}
}
另外,如果yield被用在另一个表达式中,必须放在圆括号内:
function* demo(){
console.log((yield));
console.log((yield 123));
}
next方法是可以传递参数的,看下面这个例子:
function * fn(init){
console.log(init);
console.log(yield);
console.log(yield);
}
let gen = fn("a");
gen.next("b"); // a
gen.next("c"); // c
gen.next("d"); // d
传入的b没有输出!
这是因为next有一个规则:第一次调用next()传入的值不会被调用,因为第一次调用是为了开始执行生成器函数的。
再看一个:
function* fn(){
return yield "a"
}
let gen = fn();
gen.next(); // {value: "a", done: false}
gen.next("b"); // {value: "b", done: true}
因为函数必须对整个表达式求值才可以确定要返回的值,所以在它遇到yield时暂停执行并且计算要产生的值a,然后下一次调用next方法传入了b,然后该值被确定为最终要返回的值。
迭代
因为生成器函数本身就是迭代器生成函数,所以可以直接把生成器赋值给对象的Symbol.iterator属性:
let obj = {
[Symbol.iterator] = function* (){
yield 1;
yield 2;
yield 3;
}
}
[...obj] // [1, 2, 3]
生成器返回的迭代器对象和迭代器对象的Symbol.iterator是相等的:
function *fn(){}
//遍历器对象gen
let gen = fn();
gen === gen[Symbol.iterator]() // true
for...of
使用for...of会自动遍历生成器内部的迭代器对象,而且不再需要next
function* fn(){
yield 1;
yield 2;
yield 3;
yield 4;
return 5;
}
for(let value of fn()){
console.log(value)
}
//1 2 3 4
一旦nex返回的done是true,for...of就会终止,所以没有返回5。
除此之外,扩展运算符,解构赋值和Array.from()调用的也都是迭代器接口:
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
终止生成
终止生成器的方法有 throw() 和 return()
throw
throw方法会在暂停的时候将一个错误注入到生成器对象中,如果错误没有被处理,生成器就会关闭,如果错误在内部处理了,生成器就不会关闭,继续恢复执行。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
第一个错误被生成器函数体内的catch捕获,输出内容,由于catch已经捕获过了,不会再捕捉到这个错误了,所以错误被抛出了生成器内,第二个错误属于函数题外的了。
如果生成器内部和外部都没有使用try...catch,那么程序会报错:
function* fn(){
yield console.log('hello');
yield console.log('world');
}
var g = fn();
g.next();
g.throw();
// hello
// Uncaught undefined
如果catch抛出的错误要被内部捕获,前提是至少执行一次next方法:
function* fn(){
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
var g = fn();
g.throw();
// Uncaught 1
那抛出错误之后的yield还会执行么?
function *fn(){
for(let x of [1, 2, 3]){
try {
yield x;
} catch(e) {}
yield console.log("P")
}
}
const gen = fn();
console.log(gen.next()); // { done: false, value: 1 }
gen.throw("a"); // P
console.log(gen.next()); // { done: false, vlaue: 3}
生成器在try...catch中的yield暂停了,期间throw注入一个错误a,这个错误会被yield抛出。因为错误是在生成器内部的try...catch中抛出的,所以仍然在生成器内部被捕获,接着抛出这个错误替代了要输出的的2,接下来继续执行,再次遇到yield之后输出3。在抛出错误的时候,同样不影响后面的yield进行。
return
return就相当于返回了 {done: true, value: undefined},如果return方法有值,返回的value就是那个值。
function* fn(){
yield 1;
yield 2;
}
let gen = fn();
gen.next(); // { value: 1, done: false }
gen.return("a"); // { value: "a", done: true}
gen.next(); // { value: undefined, done: true }
如果生成器里有try...finally,且正在执行try,那么return会导致直接进入finally,然后继续执行:
function* fn () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var gen = fn();
gen.next(); // { value: 1, done: false }
gen.next(); // { value: 2, done: false }
gen.return(7); // { value: 4, done: false }
gen.next(); // { value: 5, done: false }
gen.next(); // { value: 7, done: true }
其他
yield*
如果一个生成器内部调用了另一个生成器,需要手动把内部的遍历,yields* 就是为了方便这一操作的:
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
//-------
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
yield*后面的生成器函数(没有return时),等同于生成器函数内部部署一个for...of循环:
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
如果有return,就需要用一个变量来存储返回值。
因为yield每次只返回当前值,所以可以作为一个数组平铺的小思路:
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]);
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
总结
其实生成器函数依赖迭代器,内部通过yield处理过程,比较像异步操作,每一步next(),都可以去控制,这样就和Promise的功能很像了。同时有迭代器接口的数据类型都可以使用for...of、扩展运算符、解构赋值和Array.from等操作,方便了对于数据的操作,迭代器和生成器的功能还是很强大的!
参考资料:
我的公众号:道道里的前端栈,每一天一篇前端文章,嚼碎的感觉真奇妙~