重学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等操作,方便了对于数据的操作,迭代器和生成器的功能还是很强大的!