ES6中iterators与generators
要遍历处理一些数据,我们一般会用到 for 或者 for in 循环,但它们存在着一些缺点,比如,对于变量值大于循环值时,for 会出错。
另外,对于一些类似数组对象的处理,我们又不能直接采用数组的方法,需要进行格式转换,处理完后,又需要再转换成原来的格式,这样操作起来非常的繁琐。
针对这些问题,ES6 引入了 iterators 和 generators 。
一、Iterators
Iterator 简称 迭代器,它为 ES 中不同类型的数据集合提供了一种访问机制。也就是说,只要数据结构具有 Iterator 接口,便可以对该数据进行遍历操作。
其实判断是否具有 Iterator 接口,主要是看数据结构是否原生就具有 Symbol.iterator 属性,这个属性也被表示为 默认的迭代器。
我们所熟知的 数组 就天生具备这个特性。
1.1 访问默认迭代器
你可以使用 Symbol.iterator 访问对象的默认迭代器,像这样:
var arr = [1, 2, 3]; |
上面的代码访问了 values 的默认迭代器,并且使用迭代器对象下的 next()
,来迭代了数组的每一项。
因为 Symbol.iterator 被指定为 默认迭代器,那么你可以用它来判断一个对象是否可迭代:
function isIterable(object) { |
代码中的 isIterable() 函数用于检测对象是否存在默认迭代器,同时也判断了它是否为 函数。
1.2 for…of
除了上面的 next()
方法,我们还可以对具有 Symbol.iterator 属性的数据集合使用 for...of
,它可以遍历数组、Set、Map、字符串以及节点列表等类似数组的对象。
1.2.1 遍历 数组、Set、Map
var arr = ['a', 'b']; |
1.2.2 遍历 字符串
有的时候,字符串中可能包含 双字节 字符,如果用传统的 for 循环,可能无法精确获取对应位置的字符,如:
var str = "A 𠮷 B"; |
上面的代码,预期输出的值,应该为 A 空格 𠮷 空格 B
,而实际结果确实 𠮷
字符被两个乱码所替代。
但如果使用 for...of
就不会出现这样的问题了:
var str = "A 𠮷 B"; |
1.2.3 遍历 节点列表 NodeList
要操作 DOM 元素,我们通常会使用 document.querySelectorAll()
或者 document.getElementsByTagName("div")
这些原生方法来获取 DOM对象,而它们返回的 DOM对象 又是一个类似数组的 NodeList 对象,这个对象也具有 Symbol.iterator 属性,因此,我们可以直接对该对象使用 for...of
操作:
var divs = document.getElementsByTagName('div'); |
以上代码中,调用 getElementsByTagName() 去获取文档中所有 div 的 NodeList 集合,然后使用 for-of 循环输出其对应的 id,这样我们就无需先把转换为数组,再做类似的处理了。
1.2.4 迭代对象的一些方法
对于可迭代的数据集合,ES6提供了三个方法:
- entries() 返回一个包含键值对的迭代器
- values() 返回一个包含值的迭代器
- keys() 返回一个包含键的迭代器
你可以通过这其中的任何一种方法,来迭代检索集合中的每一项。下面对其逐一介绍:
entries() 方法
不同的数据类型,该方法返回的值不同,它的值是一个包含两项的数组。对于数组,第一项是数字索引。对于sets,第一项是值(因为它的键值是相同的)。对于 maps,它的第一项是键。
var arr = ['a', 'b']; |
values() 方法
这个方法只是返回集合中存储的值,但是 数组 不具备该方法:
var arr = ['a', 'b']; |
keys() 方法
该方法返回集合中的每一个键,对于数组,它只是返回 数字索引。对于 sets,因为它的 键 和 值是相同的,所以keys() 和 values() 将返回相同的内容。对于 maps,keys() 将返回独一无二的键,用代码来证实下:
var arr = ['a', 'b']; |
二、Generators
2.1 Generator函数
Generator函数是 ES6 中新引入的一种特殊函数,但它与普通的函数有些不同。在函数定义的写法上,它在关键词 function 后面增加了一个 *
符号,并且在函数体内,可以使用新的关键词 yield
。
该函数返回一个迭代器对象。来看一个最基本的 Generator函数 :
function *hello() { |
*
符号前后是否有空格不会影响 Generator函数 的定义。
通常情况下,要使用定义后的函数,可以通过函数调用的形式,但是对于 Generator函数,因为函数本身只是返回一个迭代器对象,所以你必须先调用它,然后再用这个迭代器对象的 next()
方法才能执行 Generator函数 中的代码:
function *hello() { |
让我们来分析下上面的代码,首先利用变量 hi 来保存 hello函数返回的迭代器对象,首次输出 aaa,到第一个 yield 时结束。
当第一次调用 hi.next()
时,则执行第一个 yield,接着输出一个对象,该对象包含了两个属性,value
和 done
。value
的值表示 yield 后面跟着的内容,而 done
则表示迭代还没结束。
当第二次调用 hi.next()
时,则执行第一个 yield 后面的语句,并且执行第二个 yield 的内容,直到遇到下一个 yield 后,便结束运行。此时,是先输出 bbb,然后输出的对象中的 value
是第二个 yield 后面的值,因为迭代还没结束,所以 done
的值仍然为 false 。
当第三次调用 hi.next()
时,因为函数中没有剩余的 yield 和 其他可执行的代码,所以返回的对象中 value
的值为 undefined, 并且 done
的值变为 true 了。
要注意的是,如果关键词 yield 后面没有任何内容,则 value
的值为 undefined :
function *hello() { |
2.2 yield
作为 ES6 中一个新的关键词,yield 语句只能在 Generator函数 中使用。倘若你把它用在普通函数中,则会抛出一个错误:
function hello() { |
通过前面讲到的 Generator函数 以及它的运行原理,我们可以知道,yield 语句可以分段执行 Generator函数 中的代码。只有当执行 next()
方法时,才能逐个运行 yield 语句。然后逐个返回对应的值,这一点和 return 的用法相同,但是它们之间也有差异。
yield 与 return 的异同比较
- 一个函数中可以有多个 yield 语句,但只能有一个 return 语句,即可执行多次和一次的区别
- yield 具有暂停执行函数内代码的功能,并且能够记录上一次暂停的位置,直到再次执行 next() 方法,而 return 没有这些
- yield 只能在 Generator函数 中使用,而 return 既可以使用在普通函数,也能使用在 Generator函数
- yield 和 return 都能返回其后面的值
2.3 Generator函数 返回对象的方法 - next()
其实在上面的文章,我们已经有了很多关于 next()
的描述。 主要是通过调用 next()
,来执行一个接一个的执行 yield 语句。
这里要特别增加说明的是,带参数的 next()
,我们来看下的例子:
function *sum() { |
如上面代码,与之前的 next()
调用不同的是,在这段代码中,我们每次调用 next()
时,都给它传递一个数字 5 作为参数。
第一次执行 next(5)
,返回的 value
的值为 2。即 b = yield 2
运行的结果。
第二次执行 next(5)
,返回的 value
的值为 4。即 yield a + 3
运行的结果。
第三次执行 next(5)
,返回的 value
的值为 8。这个结果有点出乎我们所料,此时执行的是 yield b + 3
,而 b 的值是 yield 2
,按理来说应该是 2 + 3 等于 5 才对。但是,由于 next()
方法传递了参数 5,此时上一个 yield 的值被这个参数所覆盖,所以才出现 5 + 3 等于 8 这样的结果 。
第四次执行 next(5)
,返回的 value
的值为 1。即 yield 1
运行的结果。
综上所述,next()
方法传递参数只会影响那些有 yield 参与操作的 yield 语句。
2.4 Generator函数 返回对象的方法 - return()
除了上面说的 next()
,Generator函数 返回的对象中还有 return()
方法,该方法返回一个特定的值,并且结束迭代当前 Generator函数 中的 yield 语句。
来看个相关的例子:
function *hello() { |
第一次调用 next()
,正常返回对应的对象。
接着调用 return()
,此时返回的对象中 value 属性值为 undefined,done 属性值则为 true,即表示停止迭代了。
当后面再次调用 next()
,后面返回的对象都是 {value: undefined, done: true}
了。
和 next()
类似,return()
函数也可以接收一个参数,此时,调用该方法后,返回的对象中 value 的属性值就是该参数,而其他的基本不变:
function *hello() { |
2.5 Generator函数 返回对象的方法 - throw()
最后,Generator函数 返回的对象中还有一个 throw()
方法。
在 JavaScript 中,throw()
方法通常与 try…catch 结合使用,主要用于当程序运行出错时,我们可以自定义一个错误对象,然后向控制台抛出一个错误:
var number = prompt('输入一个 0 到 10 之间的数', ''); |
上述代码检测用户输入的值,然后根据值的大小来定义不同的错误,最后反馈对应的错误。
以下代码演示了 Generator函数 返回对象中 throw()
方法的用法:
function *error() { |
运行 err.throw('abc')
时,错误在 Generator函数 内部被捕获。当再次运行 err.throw('def')
,错误被函数外部的 catch 所捕获。
2.6 Generator函数 作为 对象方法
要将 Generator函数 定义成 对象中的方法,可采用如下写法:
var person = { |
通过前面的 对象 章节,我们知道,在对象字面量中定义方法可以采用缩写形式,于是,person 中的 showName 可以这样改写:
var person = { |
以上两段定义 person 对象的代码是等价的。
2.7 Generator函数 与 异步操作
利用 Generator函数 中的 yield,我们可以很方便的处理一些异步操作,而无需使用一层或多层回调函数,如下面的例子:
function *paint() { |
上面的代码,定义了一个 paint
这个 Generator函数,在这个函数中,getData函数通过类似ajax的方式获取到数据。render = paint()
这行表示将返回的遍历器赋值给 render,当调用 render.next()
时,将会获取到数据,并将返回的数据赋值给了 resp。然后我们接着调用 render.next()
,此时,便会执行 Generator函数 中其余的代码。
试想下,如果你需要的数据需要通过多个ajax才能拿到,那你肯定少不了层层嵌套的回调函数。但使用 yield 来编写,则显得非常的直观和简洁。