前面的话
用循环语句迭代数据时,必须要初始化一个变量来记录每一次迭代在数据集合中的位置,而在许多编程语言中,已经开始通过程序化的方式用迭代器对象返回迭代过程中集合的每一个元素
迭代器的使用可以极大地简化数据操作,于是ES6也向JS中添加了这个迭代器特性。新的数组方法和新的集合类型(如Set集合与Map集合)都依赖迭代器的实现,这个新特性对于高效的数据处理而言是不可或缺的,在语言的其他特性中也都有迭代器的身影:新的for-of循环、展开运算符(...),甚至连异步编程都可以使用迭代器
本文将详细介绍ES6中的迭代器(Iterator)和生成器(Generator)
引入
下面是一段标准的for循环代码,通过变量i来跟踪colors数组的索引,循环每次执行时,如果i小于数组长度len则加1,并执行下一次循环
var colors = ["red", "green", "blue"]; for (var i = 0, len = colors.length; i < len; i++) { console.log(colors[i]); }
虽然循环语句语法简单,但如果将多个循环嵌套则需要追踪多个变量,代码复杂度会大大增加,一不小心就错误使用了其他for循环的跟踪变量,从而导致程序出错。迭代器的出现旨在消除这种复杂性并减少循环中的错误
迭代器
迭代器是一种特殊对象,它具有一些专门为迭代过程设计的专有接口,所有的迭代器对象都有一个next()方法,每次调用都返回一个结果对象。结果对象有两个属性:一个是value,表示下一个将要返回的值;另一个是done,它是一个布尔类型的值,当没有更多可返回数据时返回true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次next()方法,都会返回下一个可用的值
如果在最后一个值返回后再调用next()方法,那么返回的对象中属性done的值为true,属性value则包含迭代器最终返回的值,这个返回值不是数据集的一部分,它与函数的返回值类似,是函数调用过程中最后一次给调用者传递信息的方法,如果没有相关数据则返回undefined
下面用ES5的语法创建一个迭代器
function createIterator(items) { var i = 0; return { next: function() { var done = (i >= items.length); var value = !done ? items[i++] : undefined; return { done: done, value: value }; } }; } var iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在上面这段代码中,createIterator()方法返回的对象有一个next()方法,每次调用时,items数组的下一个值会作为value返回。当i为3时,done变为true;此时三元表达式会将value的值设置为undefined。最后两次调用的结果与ES6迭代器的最终返回机制类似,当数据集被用尽后会返回最终的内容
上面这个示例很复杂,而在ES6中,迭代器的编写规则也同样复杂,但ES6同时还引入了一个生成器对象,它可以让创建迭代器对象的过程变得更简单
生成器
生成器是一种返回迭代器的函数,通过function关键字后的星号(*)来表示,函数中会用到新的关键字yield。星号可以紧挨着function关键字,也可以在中间添加一个空格
// 生成器 function *createIterator() { yield 1; yield 2; yield 3; } // 生成器能像正规函数那样被调用,但会返回一个迭代器 let iterator = createIterator(); console.log(iterator.next().value); // 1 console.log(iterator.next().value); // 2 console.log(iterator.next().value); // 3
在这个示例中,createlterator()前的星号表明它是一个生成器;yield关键字也是ES6的新特性,可以通过它来指定调用迭代器的next()方法时的返回值及返回顺序。生成迭代器后,连续3次调用它的next()方法返回3个不同的值,分别是1、2和3。生成器的调用过程与其他函数一样,最终返回的是创建好的迭代器
生成器函数最有趣的部分是,每当执行完一条yield语句后函数就会自动停止执行。举个例子,在上面这段代码中,执行完语句yield 1之后,函数便不再执行其他任何语句,直到再次调用迭代器的next()方法才会继续执行yield 2语句。生成器函数的这种中止函数执行的能力有很多有趣的应用
使用yield关键字可以返回任何值或表达式,所以可以通过生成器函数批量地给迭代器添加元素。例如,可以在循环中使用yield关键字
function *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在此示例中,给生成器函数createlterator()传入一个items数组,而在函数内部,for循环不断从数组中生成新的元素放入迭代器中,每遇到一个yield语句循环都会停止;每次调用迭代器的next()方法,循环会继续运行并执行下一条yield语句
生成器函数是ES6中的一个重要特性,可以将其用于所有支持函数使用的地方
【使用限制】
yield关键字只可在生成器内部使用,在其他地方使用会导致程序抛出错误
function *createIterator(items) { items.forEach(function(item) { // 语法错误 yield item + 1; }); }
从字面上看,yield关键字确实在createlterator()函数内部,但是它与return关键字一样,二者都不能穿透函数边界。嵌套函数中的return语句不能用作外部函数的返回语句,而此处嵌套函数中的yield语句会导致程序抛出语法错误
【生成器函数表达式】
也可以通过函数表达式来创建生成器,只需在function关键字和小括号中间添加一个星号(*)即可
let createIterator = function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } }; let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之后的所有调用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在这段代码中,createlterator()是一个生成器函数表达式,而不是一个函数声明。由于函数表达式是匿名的,因此星号直接放在function关键字和小括号之间。此外,这个示例基本与前例相同,使用的也是for循环
[注意]不能用箭头函数来创建生成器
【生成器对象的方法】
由于生成器本身就是函数,因而可以将它们添加到对象中。例如,在ES5风格的对象字面量中,可以通过函数表达式来创建生成器
var o = { createIterator: function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; let iterator = o.createIterator([1, 2, 3]);
也可以用ES6的函数方法的简写方式来创建生成器,只需在函数名前添加一个星号(*)
var o = { *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; let iterator = o.createIterator([1, 2, 3]);
这些示例使用了不同于之前的语法,但它们的功能实际上是等价的。在简写版本中,由于不使用function关键字来定义createlterator()方法,因此尽管可以在星号和方法名之间留白,但还是将星号紧贴在方法名之前
可迭代对象
可迭代对象具有Symbol.iterator属性,是一种与迭代器密切相关的对象。Symbol.iterator通过指定的函数可以返回一个作用于附属对象的迭代器。在ES6中,所有的集合对象(数组、Set集合及Map集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。ES6中新加入的特性for-of循环需要用到可迭代对象的这些功能
[注意]由于生成器默认会为Symbol.iterator属性赋值,因此所有通过生成器创建的迭代器都是可迭代对象
一开始,我们曾提到过循环内部索引跟踪的相关问题,要解决这个问题,需要两个工具:一个是迭代器,另一个是for-of循环。如此一来,便不需要再跟踪整个集合的索引,只需关注集合中要处理的内容
for-of循环每执行一次都会调用可迭代对象的next()方法,并将迭代器返回的结果对象的value属性存储在一个变量中,循环将持续执行这一过程直到返回对象的done属性的值为true。这里有个示例
let values = [1, 2, 3]; for (let num of values) { //1 //2 //3 console.log(num); }
这段for-of循环的代码通过调用values数组的Symbol.iterator方法来获取迭代器,这一过程是在JS引擎背后完成的。随后迭代器的next()方法被多次调用,从其返回对象的value属性读取值并存储在变量num中,依次为1、2和3,当结果对象的done属性值为true时循环退出,所以num不会被赋值为undefined
如果只需迭代数组或集合中的值,用for-of循环代替for循环是个不错的选择。相比传统的for循环,for-of循环的控制条件更简单,不需要追踪复杂的条件,所以更少出错
[注意]如果将for-of语句用于不可迭代对象、null或undefined将会导致程序抛出错误
【访问默认迭代器】
可以通过Symbol.iterator来访问对象默认的迭代器
let values = [1, 2, 3]; let iterator = values[Symbol.iterator](); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }"
在这段代码中,通过Symbol.iterator获取了数组values的默认迭代器,并用它遍历数组中的元素。在JS引擎中执行for-of循环语句时也会有类似的处理过程
由于具有Symbol.iterator属性的对象都有默认的迭代器,因此可以用它来检测对象是否为可迭代对象
function isIterable(object) { return typeof object[Symbol.iterator] === "function"; } console.log(isIterable([1, 2, 3])); // true console.log(isIterable("Hello")); // true console.log(isIterable(new Map())); // true console.log(isIterable(new Set())); // true console.log(isIterable(new WeakMap())); // false console.log(isIterable(new WeakSet())); // false
这里的islterable()函数可以检查指定对象中是否存在默认的函数类型迭代器,而for-of循环在执行前也会做相似的检查
除了使用内建的可迭代对象类型的Symbol.iterator,也可以使用Symbol.iterator来创建属于自己的迭代器
【创建可迭代对象】
默认情况下,开发者定义的对象都是不可迭代对象,但如果给Symbol.iterator属性添加一个生成器,则可以将其变为可迭代对象
let collection = { items: [], *[Symbol.iterator]() { for (let item of this.items) { yield item; } } }; collection.items.push(1); collection.items.push(2); collection.items.push(3); for (let x of collection) { //1 //2 //3 console.log(x); }
在这个示例中,先创建一个生成器(注意,星号仍然在属性名前)并将其赋值给对象的Symbol.iterator属性来创建默认的迭代器;而在生成器中,通过for-of循环迭代this.items并用yield返回每一个值。collection对象默认迭代器的返回值由迭代器this.items自动生成,而非手动遍历来定义返回值
【展开运算符和非数组可迭代对象】
通过展开运算符(...)可以把Set集合转换成一个数组
let set = new Set([1, 2, 3, 3, 3, 4, 5]), array = [...set]; console.log(array); // [1,2,3,4,5]
这段代码中的展开运算符把Set集合的所有值填充到了一个数组字面量里,它可以操作所有可迭代对象,并根据默认迭代器来选取要引用的值,从迭代器读取所有值。然后按照返回顺序将它们依次插入到数组中。Set集合是一个可迭代对象,展开运算符也可以用于其他可迭代对象
let map = new Map([ ["name", "huochai"], ["age", 25]]), array = [...map]; console.log(array); // [ ["name", "huochai"], ["age", 25]]
展开运算符把Map集合转换成包含多个数组的数组,Map集合的默认迭代器返回的是多组键值对,所以结果数组与执行new Map()时传入的数组看起来一样
在数组字面量中可以多次使用展开运算符,将可迭代对象中的多个元素依次插入新数组中,替换原先展开运算符所在的位置
let smallNumbers = [1, 2, 3], bigNumbers = [100, 101, 102], allNumbers = [0, ...smallNumbers, ...bigNumbers]; console.log(allNumbers.length); // 7 console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
创建一个变量allNumbers,用展开运算符将smallNumbers和bigNumbers里的值依次添加到allNumbers中。首先存入0,然后存入small中的值,最后存入bigNumbers中的值。当然,原始数组中的值只是被复制到allNumbers中,它们本身并未改变
由于展开运算符可以作用于任意可迭代对象,因此如果想将可迭代对象转换为数组,这是最简单的方法。既可以将字符串中的每一个字符(不是编码单元)存入新数组中,也可以将浏览器中NodeList对象中的每一个节点存入新的数组中
内建迭代器
迭代器是ES6的一个重要组成部分,在ES6中,已经默认为许多内建类型提供了内建迭代器,只有当这些内建迭代器无法实现目标时才需要自己创建。通常来说当定义自己的对象和类时才会遇到这种情况,否则,完全可以依靠内建的迭代器完成工作,而最常使用的可能是集合的那些迭代器
【集合对象迭代器】
在ES6中有3种类型的集合对象:数组、Map集合与Set集合
为了更好地访问对象中的内容,这3种对象都内建了以下三种迭代器
entries() 返回一个迭代器,其值为多个键值对
values() 返回一个迭代器,其值为集合的值
keys() 返回一个迭代器,其值为集合中的所有键名
调用以上3个方法都可以访问集合的迭代器
entries()迭代器
每次调用next()方法时,entries()迭代器都会返回一个数组,数组中的两个元素分别表示集合中每个元素的键与值。如果被遍历的对象是数组,则第一个元素是数字类型的索引;如果是Set集合,则第一个元素与第二个元素都是值(Set集合中的值被同时作为键与值使用);如果是Map集合,则第一个元素为键名
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let entry of colors.entries()) { console.log(entry); } for (let entry of tracking.entries()) { console.log(entry); } for (let entry of data.entries()) { console.log(entry); }
调用console.log()方法后输出以下内容
[0, "red"] [1, "green"] [2, "blue"] [1234, 1234] [5678, 5678] [9012, 9012] ["title", "Understanding ES6"] ["format", "ebook"]
在这段代码中,调用每个集合的entries()方法获取一个迭代器,并使用for-of循环来遍历元素,且通过console将每一个对象的键值对输出出来
values()迭代器
调用values()迭代器时会返回集合中所存的所有值
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let value of colors.values()) { console.log(value); } for (let value of tracking.values()) { console.log(value); } for (let value of data.values()) { console.log(value); }
调用console.log()方法后输出以下内容
"red" "green" "blue" 1234 5678 9012 "Understanding ES6" "ebook"
如上所示,调用values()迭代器后,返回的是每个集合中包含的真正数据,而不包含数据在集合中的位置信息
keys()迭代器
keys()迭代器会返回集合中存在的每一个键。如果遍历的是数组,则会返回数字类型的键,数组本身的其他属性不会被返回;如果是Set集合,由于键与值是相同的,因此keys()和values()返回的也是相同的迭代器;如果是Map集合,则keys()迭代器会返回每个独立的键
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let key of colors.keys()) { console.log(key); } for (let key of tracking.keys()) { console.log(key); } for (let key of data.keys()) { console.log(key); }
调用console.log()方法后输出以下内容
0 1 2 1234 5678 9012 "title" "format"
keys()迭代器会获取colors、tracking和data这3个集合中的每一个键,而且分别在3个for-of循环内部将这些键名打印出来。对于数组对象来说,无论是否为数组添加命名属性,打印出来的都是数字类型的索引;而for-in循环迭代的是数组属性而不是数字类型的索引
不同集合类型的默认迭代器
每个集合类型都有一个默认的迭代器,在for-of循环中,如果没有显式指定则使用默认的迭代器。数组和Set集合的默认迭代器是values()方法,Map集合的默认迭代器是entries()方法。有了这些默认的迭代器,可以更轻松地在for-of循环中使用集合对象
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "print"); // 与使用 colors.values() 相同 for (let value of colors) { console.log(value); } // 与使用 tracking.values() 相同 for (let num of tracking) { console.log(num); } // 与使用 data.entries() 相同 for (let entry of data) { console.log(entry); }
上述代码未指定迭代器,所以将使用默认的迭代器。数组、Set集合及Map集合的默认迭代器也会反应出这些对象的初始化过程,所以这段代码会输出以下内容
"red" "green" "blue" 1234 5678 9012 ["title", "Understanding ES6"] ["format", "print"]
默认情况下,如果是数组和Set集合,会逐一返回集合中所有的值。如果是Map集合,则按照Map构造函数参数的格式返回相同的数组内容。而WeakSet集合与WeakMap集合就没有内建的迭代器,由于要管理弱引用,因而无法确切地知道集合中存在的值,也就无法迭代这些集合了
【字符串迭代器】
自ES5发布以后,JS字符串慢慢变得更像数组了,例如,ES5正式规定可以通过方括号访问字符串中的字符(也就是说,text[0]可以获取字符串text的第一个字符,并以此类推)。由于方括号操作的是编码单元而非字符,因此无法正确访问双字节字符
var message = "A