ES6 迭代器 iterator 介绍和使用
生成器概念在 Java,Python 等语言中都是具备的,ES6 也添加到了 JavaScript 中。Iterator 可以使我们不需要初始化集合,以及索引的变量,而是使用迭代器对象的 next 方法,返回集合的下一项的值,偏向程序化。
ES5 中遍历集合通常都是 for 循环,数组还有 forEach 方法,对象就是 for-in,而迭代器可以统一处理所有集合数据的方法。迭代器是一个接口,只要你这个数据结构暴露了一个 iterator 的接口,那就可以完成迭代。ES6 创造了一种新的遍历命令 for...of 循环,Iterator 接口主要供 for...of 消费。
ES5 中的 loop
for 循环
var arr = [1, 2, 3]; for(var i = 0, len = arr.length; i < len; i++) { console.log(arr[i]); }
这是标准 for 循环的写法,字符串也支持,定义一个变量 i 作为索引,以跟踪访问的位置,len 是数组的长度,条件就是 i 不能超过 len。
这种写法看起来算是比较简单明了的,只不过 for 里面要写一大坨,而且当出现嵌套的时候,要定义多个变量去维护不同集合的索引,增加复杂度,容易出错。
forEach
forEach 是数组内置方法,写起来比较简洁,问题就是不能中断,跳出循环。而这个是我们经常会遇见的,达到某个条件就不需要往后遍历了。
var arr = [1, 2, 3];
arr.forEach(item => console.log(item))
for-in
常用来遍历对象,可以获得对象的 key值,但是只能提取 key,value 需要我们自己 obj[key] 的形式去访问。
var obj = { name: 'syj', age: 24, sex: 'male', hobby: 'girl', } for(var key in obj) { console.log(key); }
什么是迭代器
迭代器是带有特殊接口的对象。含有一个 next() 方法,调用返回一个包含两个属性的对象,分别是 value 和 done,value 表示当前位置的值,done 表示是否迭代完,当为 true 的时候,调用 next 就无效了。
ES5 模拟一个迭代器
function createIterator(ary) { var i = 0; return { next: function() { return { value: ary[i++], done: i > ary.length } } } } var iterator = createIterator(['a', 'b', 'c']) var done = false; while (!done) { var result = iterator.next(); console.log(result); done = result.done; } //{ value: 'a', done: false } //{ value: 'b', done: false } //{ value: 'c', done: false } //{ value: undefined, done: true }
createIterator 可以看做一个返回迭代器对象的工厂函数,通过 while 循环调用返回的 iterator.next() 会得到 result,直到 done 变为 true。用 ES5 写还是比较麻烦的,而且我们这样写并不支持 for...of。很快就会看到 ES6 真正的写法啦。
迭代器协议 Iteration protocols
迭代器对象不是新的语法或新的内置对象,而一种协议(迭代器协议),所有遵守这个协议的对象,都可以称之为迭代器。也就是说我们上面ES5的写法得到的对象遵循迭代器协议,即包含 next,调用 next 返回一个 result{value, done}。
可迭代类型
ES6 还引入了一个新的 Symbol 对象,symbol 值是唯一的。定义了一个 Symbol.iterator 属性,只要对象中含有这个属性,就是可迭代的,可用于 for...of。在 ES6 中,所有的集合对象,包括数组,Map 和 Set,还有字符串都是可迭代的,因为他们都有默认的迭代器。
尝试使用 for...of
let ary = ['a', 'b', 'c']; //数组 let str = 'str'; //字符串 let set = new Set(); //Set set.add('s'); set.add('s'); set.add('e'); set.add('t'); let map = new Map(); //Map let o = {}; map.set('m', 'm'); map.set(o, 'object'); let obj = { //对象 name: 'syj', age: 24 } function forOf(list) { for(var value of list) { console.log(value); } } forOf(ary); //a b c forOf(str); // s t r forOf(set); // s e t forOf(map); //[ 'm', 'm' ], [ {}, 'object' ] forOf(obj); //TypeError: list[Symbol.iterator] is not a function
通过结果可以看出,确实集合类型和字符串都可以用默认的 for...of 来迭代。但是对象却不可以,内部抛出一个错误,找不到迭代器的接口,证实了上面的言论。
也许你不了解 Set、Map、Set 通常是类似于数组的,无重复项的集合,Map 是类似于对象的,但是他的key可以是任何类型,增强版的“键值对”的数据结构。
可以访问默认的 iterator
数组中默认的 iterator 我们是可以访问的,用法是一样的。
let ary = ['a', 'b', 'c']; let iterator = ary[Symbol.iterator](); console.log(iterator.next()); //{ value: 'a', done: false } console.log(iterator.next()); //{ value: 'b', done: false } console.log(iterator.next()); //{ value: 'c', done: false } console.log(iterator.next()); //{ value: undefined, done: true }
使对象可迭代
前面说了只要对象包含 [Symbol.iterator] 的属性,就可以通过 for...of 遍历。我们也可以在对象中添加该属性。
我们用两种方式
const obj = { b: 2 } const a = 'a' obj.a = 1; Object.defineProperty(obj, Symbol.iterator, { enumerable: false, writable: false, configurable: true, value: function () { const that = this; let index = 0; const ks = Object.keys(that); return { next: function() { return { value: that[ks[index++]], done: (index > ks.length) } } } } }) for(const v of obj) { console.log(v); // 2 , 1 }
通过 defineProperty 向 obj 对象中添加 [Symbol.iterator],我们在对应的 value 做的就是通过 Object.keys 取出它的 key,然后调用一次 next 就往后找一位,可以通过 next() 尝试一下。因为 obj 有了 [Symbol.iterator],for...of 可以找到,并且调用。
const fibonacci = { [Symbol.iterator]: function () { let [pre, next] = [0, 1]; return { next() { [pre, next] = [next, pre + next]; return { value: next, done: next > 1000 } } } } } for(var n of fibonacci) { console.log(n) } // 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610,987
通过直接声明给对象定义一个 iterator。这里是 iterator 经典的用法。当这个数据集合特别大,甚至无限的时候,我们要把它定义好,取出来都是很庞大的操作,这时候 iterator 的优势就很明显了。只有调用一次 next,才返回下一个值,不调用就没有。假如说我们给 done 没加限制条件,这个迭代器就永远没有 done=true,就会永远可以 next。为了防止 for...of ,调用 next 的时候,有可能让我的电脑卡死,限制在 1000 以内。
还有一些值得关注的点
拓展运算符与 iterator 关系密切
借用我们 fibonacci 的例子,只有含有 [Symbol.iterator] 属性的对象,才可以使用 ... 展开。
const fibonacci = { a: 'a', b: 'b', } console.log(...fibonacci); //TypeError: //(var)[Symbol.iterator] is not a function fibonacci[Symbol.iterator] = function () { let [pre, next] = [0, 1]; return { next() { [pre, next] = [next, pre + next]; return { value: next, done: next > 1000 } } } } console.log(...fibonacci);//
第一次 log 我们尝试把 fibonacci 使用... 展开,但是会报错。把这行 console 注释掉以后,给这个对象添加一个 Symbol.iterator 属性,再次使用... ,就会得到之前的斐波那契数列了。 由于 ES6 的数组等集合默认有 Symbol.iterator 属性,所以我们都是可以直接使用扩展运算符。
类数组
ES6 中对于类数组,也添加了默认迭代器,比如 NodeList,它和数组的默认迭代器的用法是一样的。这意味着你可以使用 for...of 循环或在任何对象默认迭代器的内部来迭代 NodeList
var divs = document.getElementsByTagName("div"); for (const element of divs) { console.log(element.id); }
总结
ES6 提出迭代器的概念,契合了JS语言发展的趋势,统一处理数据结构。迭代器是 ES6 中很重要的部分,我们仅仅使用是很方便的,但是自定义一些 iterator,或者更复杂的方式运行迭代器,还需要我们继续学习。
而定义一个对象的迭代器,又与 Symbol 对象有关,它采用了 Symbol 里面的一个默认属性 iterator,用来访问对象的迭代器。最后可迭代的数据类型,我们都可以用 for...of 方法循环遍历,而且集合和字符串内置迭代器,我们可以轻松方便的访问。拓展运算符也是基于 iterator 的拓展,通过...我们可以把其他数据类型转化为数组,因为...通过执行迭代器,并读取返回的 value。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: 动手实现一个最简单的 redux
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
@liuyiliuyi 是的,感谢... 已修改。我也不知道我当时为啥写了这句话- -
for in “其他数组等类型都不行。“那一块写错了吧, 数组也是可以for in 的, 可以取到对应的索引。