ES2015增加的这两个特性之前一直没有很明白,或者是说看完教程就很容易忘记,这次通过写一篇博文来增强理解与记忆
Iterator
Iterator中文名为迭代器,顾名思义是用来循环遍历的,我们平时说到循环遍历肯定会想到for、while循环以及数组的内建方法。for、while循环就不多说了,这是语言中最基础的循环迭代方法,缺点是对于很简单的迭代需要写一些模板代码,不够易用。所以对于那些常用的需要遍历的数据类型比如Array,JS提供了forEach,map,filter等方便且附带特定功能的方法。当然这些方法都属于数组,对于另外的我们想遍历的数据结构比如字符串就没办法使用了,于是ES2015又加入了for...of循环,可以用来遍历任意的可迭代对象。
那么问题来了,这个可迭代对象是什么呢?这就涉及到ES2015加入的新概念,可迭代协议(iterable protocol)与迭代器协议(iterator protocol)。
可迭代协议
可迭代协议规定可迭代对象必须实现一个叫
Symbol.iterator
的方法,该方法必须返回一个迭代器对象。上面的问题就有了答案,只要一个对象本身或者原型链中具有这个方法,那么这个对象就是可迭代的。// 这样的对象就是一个可迭代对象 const iterableObj = { // 必须有该方法 [Symbol.iterator]() { return
; //该方法必须返回迭代器对象 } }; 迭代器对象又是什么呢?这就是迭代器协议的内容了。
迭代器协议
迭代器协议规定迭代器对象必须有一个
next
方法,该方法必须返回一个具有done
与value
属性的对象。这样说比较抽象,以下代码示例可以形象的展示迭代器对象:// 这样的对象就是迭代器对象 const itratorObj = { next() { return { done: false, // 当该值为ture时迭代结束 value: null, // 每次迭代返回的值,当done为true时可以省略 }; } };
弄清楚了上面两个基本概念后,我们就理解了何为可迭代对象。有些内置对象自己实现了可迭代协议,所以他们是可迭代的(可以用for...of遍历),比如Array,String,Set,Map等。当然我们自己定义的对象也可以是可迭代的,只要我们自己实现可迭代协议。下面就举一个例子。
const range = {
from: 1,
to: 5,
};
我们有以上对象,如果现在直接使用for...of循环去遍历,那肯定是报错的,错误信息为range不可迭代。我们想让他变的可迭代并且按照我们自己想要的方式迭代:从from迭代到to,仅需要实现可迭代协议即可。
const range = {
form: 1,
to: 5,
[Symbol.iterator]() {
// 如何实现?
return {
next() {
// 下面的两个属性什么时候变化?
return {
done: false,
value: 1,
};
}
};
}
};
说起来轻巧,但是这个代码要怎么实现呢?这就涉及到Symbol.iterator
方法的运行机制了。
当一个对象进行迭代的时候,在迭代之前会先执行Symbol.iterator
方法,然后使用该方法返回的迭代器对象来进行迭代控制。通俗来说就是,每次迭代的时候都执行一次next
方法,把value
值作为该次迭代的返回值,当done
为true时迭代结束。顺着这个思路就可以实现我们的自定义迭代方法了。
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let cur = this.from; // 迭代前初始化一个当前迭代值
const to = this.to; // 暂存终止值
return {
next() {
const done = cur > to; // 当迭代值大于to的时候迭代结束
return {
done,
value: done ? undefined : cur++, // 将迭代值返回
};
}
};
}
};
// 验证
for (let item of range) {
// 这里需要注意:for...of此类的迭代操作会忽略done为true的值
console.log(item); // 1 2 3 4 5
}
一般来说,推荐对象既实现可迭代协议,也实现迭代器协议,这样被称为格式良好的可迭代对象,就像这样:
const range = {
from: 1,
to: 5,
// 对象具有next方法,符合迭代器协议
next() {
const done = this._iteratorVal > this.to;
return {
done,
value: done ? undefined : this._iteratorVal++,
};
},
[Symbol.iterator]() {
this._iteratorVal = this.from;
return this; // 可迭代协议返回对象本身
}
};
我们可以实现一个用于生成可迭代对象forEach的通用方法,更方便的去迭代非数组的可迭代对象。
function iterableForEachFactory(obj) {
return function (callback) {
let index = 0;
for (let item of obj) {
callback(item, index);
index++;
}
}
}
const rangeForEach = iterableForEachFactory(range);
rangeForEach((item, index) => console.log(item, index)); // 1 2 3 4 5, 0 1 2 3 4
到目前为止,我们只在对象中使用迭代器,有时候我们仅仅只需要一个迭代器去进行自定义的迭代逻辑,类似于for、while循环,但是for while循环是自动的,一旦开始迭代就无法暂停,只能彻底跳出循环。利用迭代器协议,我们能实现一个可以暂停的手动循环,并且能自定义循环逻辑。
function iteratorMaker(param) {
let index = 0;
let lastValue = null;
// param可以是代表迭代次数的数字,也可以是自定义迭代逻辑的函数,也可以不传
const isFunction = typeof param === 'function';
const isNumber = typeof param === 'number';
return {
next() {
const nextIndex = index++;
lastValue = isFunction ? param(lastValue, nextIndex) : nextIndex;
// 如果传入了自定义迭代函数,那么返回undefined可以终止循环
const done = isFunction ?
lastValue === undefined :
isNumber ? nextIndex >= param : false;
return {
done,
value: done ? undefined : lastValue,
};
}
};
}
// 循环两次的普通迭代器
const iteratorCircle2 = iteratorMaker(2);
iteratorCircle2.next().value; // 0
iteratorCircle2.next().value; // 1
iteratorCircle2.next().value; // undefined
// 每次翻倍的无穷迭代器
const iteratorDoubleCircle = iteratorMaker(doubleCircle);
function doubleCircle(lastValue) {
return lastValue ? lastValue * 2 : 1;
}
iteratorDoubleCircle.next().value; // 1
iteratorDoubleCircle.next().value; // 2
iteratorDoubleCircle.next().value; // 4
iteratorDoubleCircle.next().value; // 8
Generator
前文详细说明了Iterator的作用与用法,接下来理解并使用Generator(生成器)就非常简单了。可以这么简单粗暴的理解:Generator就是Iterator的语法糖,但是它有更高级的功能。他们的关系很容易让人想到Proxy与Reflect,Reflect可以完美配合Proxy的语法,它也有自己额外的功能。
语法
function* gen() { yield 1; yield 2; yield 3; return; // return是可选的,相当于done: true } // 这个就是generator对象,它既实现了可迭代协议,也实现了迭代器协议 const g = gen(); typeof g[Symbol.iterator]; // function typeof g.next; // function g[Symbol.iterator]() === g; // true
看到这里是不是很眼熟,没错,generator对象和我们之前实现的格式良好的可迭代对象range是很类似的。
我们可以很容易的将range的例子使用generator重写一下:
const range = { from: 1, to: 5, *[Symbol.iterator]() { for (let i = this.from; i <= this.to; i++) { yield i; } } }; // 验证 for (let item of range) { console.log(item); // 1 2 3 4 5 }
组合
Generator可以嵌套进行组合,来实现各种各样的功能。
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { // 0..9 yield* generateSequence(48, 57); // 组合的语法为yield*后面跟任意generator函数 // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } console.log(str); // 0..9A..Za..z
也可以使用组合递归来实现数组打平
const arr = [1, 2, 3, [4, 5, [6, 7, 8, [9]]]]; function* flatten(arr) { for (let item of arr) { if (Array.isArray(item)) { yield* flatten(item); } else { yield item; } } } const flatArr = [...flatten(arr)]; console.log(flatArr); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
双向yield
细心的读者会发现,到目前为止,我们使用generator是实现不了上文中
iteratorMaker
函数的,这里我们就需要介绍generator的一个最强大的概念:双向yield。yield
给人的感觉是与iterator中的value
一样,向外部迭代输出值,但是它还可以接收外部next
函数传入的值,作为下一次执行的输出结果。function* gen() { const result = yield 1; // 将yield表达式赋值给一个变量就实现了双向yield yield result * 2; // 下一次next函数传入的值会赋值给result } const g = gen(); g.next().value; // 1 g.next(2).value; // 4
了解这个特性之后,我们就可以对
iteratorMaker
函数用generator重写了:function iteratorMaker(param) { let index = 0; let lastValue = null; const isFunction = typeof param === 'function'; const isNumber = typeof param === 'number'; function* gen() { let passValue = null; while (isNumber ? index <= param : lastValue !== undefined) { passValue = yield (passValue === null ? lastValue : initial); } } const g = gen(); return { next() { const nextIndex = index++; lastValue = isFunction ? param(lastValue, nextIndex) : nextIndex; return g.next(lastValue); } }; }
总的来说,Iterator与Generator都是ES2015里面比较少用的特性,也有一定的复杂度,特别是Generator的双向yield,需要多动手写一写实例才能比较好的理解它,希望这篇文章能对你有用。