layout | title | description | tags | image | comments | share | ||||
---|---|---|---|---|---|---|---|---|---|---|
post |
译-Javascript中的Iterables和Iterators |
ES6迭代器介绍 |
|
|
true |
true |
迭代器是es6引入的重要概念(Java中很早就有了),也是理解generator的基础。英文原文:http://jsrocks.org/2015/09/javascript-iterables-and-iterators/
ECMAScript 2015 (ES6) 介绍了两个新的概念,它们密切相关: iterables and iterators.
希望你在阅读了这篇文章后,会了解到这两个概念的重要性,并且能在日常开发中使用它们。
可遍历对象:是指实现了Iterable interface接口的对象. 可遍历对象会暴露一个遍历方法,我们可以通过这个暴露的方法去自定义遍历行为。
请看下面的demo:
//推荐google浏览器,执行报错的话,说明你的浏览器不支持这两个新的特征,我用的是google chrome v45.0,顺利执行
let iterable = [1, 2, 3];
for (let item of iterable) {
console.log(item); // 1, 2, 3
}
let iterable2 = new Set([4, 5, 6]);
for (let item of iterable2) {
console.log(item); // 4, 5, 6
}
let iterable3 = '789';
for (let item of iterable3) {
console.log(item); // '7', '8', '9'
}
let notIterable = {name:'alibaba'};
for(let item of notIterable){
console.log(item) // 执行报错!原因很简单,普通Oject并不是可遍历对象
}
for-of 语法支持可遍历对象, 因此我们可以这种规范的遍历语法去遍历实现了Iterable接口的对象。
这里的Iterable其实是指一个[Symbol.iterator]
方法,任何对象只要包含了这个[Symbol.iterator]
方法,它就是可遍历对象。
如果你不太了解这个ES6新特征Symbol你可以看下这篇文章:阮一峰:es6 symbols(你也可以看英文文章:symbols)。
Symbol.iterator
是个一个 well-known (内置于语言内部) Symbol, 他主要用于定义可遍历对象。
在刚才的例子中,我们其实隐式的使用了[Symbol.iterator]
方法,在Array,Set,String的原型prototype里都包含了[Symbol.iterator]
方法。比如 Array, Set, String and Map 它们都定义了默认的遍历行为, 然而Object 并没这个方法。
Iterable接口允许自定义遍历行为,请看下面的例子,我们讲对象的遍历行为设置为数组的遍历行为,让对象能像数组那样遍历:
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'
}
//这个还是比较实用的,毕竟在es5中Oject遍历用for in不是很简洁。
现在你可能会问:"怎样才能自定义遍历行为?"
我们已经知道:添加一个[Symbol.iterator]
可以让一个对象变为可遍历的,但是需要注意一点的是:[Symbol.iterator]
方法必须返回一个 iterator object ,就是这个iterator object负责完成遍历逻辑。不要急,在下一个部分我们介绍它的。
在介绍iterators(迭代器)之前,让我们先来回顾下Iterable interface的概念:
- 它意味着可以给使用者一个可遍历的对象(iterable object: "嗨,我有一个
[Symbol.iterator]
方法."); - 它会提供一个标准的方法去遍历任何可遍历的对象(iterable object: "亲,你可以调用
[Symbol.iterator]
方法去完成遍历过程").
迭代器对象是指实现了Iterator interface的对象,迭代器对象必须拥有一个next
方法,并且这个方法的返回值必须要是 { value: Any, done: Boolean }
这种格式的对象,当我们第一次调用next方法时,next方法会返回第一个遍历结果元素。这个done
属性是指当前遍历的元素是否是最后一个元素,如果done:true意味着所有遍历完成。
下面例子介绍如何使用遍历器去遍历数组:
let iterable = ['a', 'b', 'c'];
// 获取遍历器
let iterator = iterable[Symbol.iterator]();
//使用next方法遍历元素(好像java遍历器)
iterator.next(); // { value: 'a', done: false }
iterator.next(); // { value: 'b', done: false }
iterator.next(); // { value: 'c', done: false }
iterator.next(); // { value: undefined, done: true }
最开始的例子我们不是使用for-of的语法么!现在我们来看一下for-of语法的内部实现方式:
let iterable = ['a', 'b', 'c'];
// 简便语法,利用for-of
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
// for-of的内部实现方式,是不是感觉很简单!真的好像java遍历器
for (let _iterator = iterable[Symbol.iterator](), _result, item; _result = _iterator.next(), item = _result.value, !_result.done;) {
console.log(item); // 'a', 'b', 'c'
}
现在我们知道了iterables和iterators后,现在我们可以创建自定义遍历行为的可遍历对象了。首先,我们先来自己实现一个类似数组遍历行为的遍历器:
let iterable = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
let value = this[index];
let done = index === this.length;
index++;
return { value, done };
}
};
}
};
//使用for-of语法完成遍历
for (let item of iterable) {
console.log(item); // 'a', 'b', 'c'
}
额,这个看起来是有点复杂,不清晰,但是不要慌,让我们仔细的分析下它。
我们一开始用JavaScript本文化特征语法创建了一个对象。并且用 computed properties 和 shorthand methods ES2015 本文化扩展语法给这个对象定义了一个[Symbol.iterator]
方法,这个对象有了[Symbol.iterator]
方法,因此他也成了一个可遍历对象。
这个[Symbol.iterator]
方法实现了默认的遍历行为,这个方法返回了一个遍历器对象,这个遍历器对象包含了上文我们说的next方法,所以我们才能把[Symbol.iterator]
方法返回的对象视为遍历器对象。
其中next
方法里面用到了arrow function,所以next方法里的this才会指向iterable对象。(个人来说,我认为ES6 中箭头函数是最强大,最实用,最能简化现有js开发的新特征之一,个人看法,不喜勿喷)。
next方法返回了一个由value和done属性组成的对象,语法很简洁,很实用,用的是shorthand properties语法,简单来说{value,done}就等于{'value':value,'done':done}。
看到这里,相信你已经完全上面例子里每行代码是怎么工作的了。for-of
语法会调用 [Symbol.iterator]
方法来获取遍历器对象,遍历对象里有一个next方法,通过调用next方法遍历元素。
恩,总之是需要的。下面我讲列出最常见的���题和疑惑,并以QA的方式展现给大家。
原因是:如果直接是一个遍历器,当你同时想多次遍历时,就不能实现了,同时多次遍历这个可能有点难理解,请看下面的代码:
let iterable = [1, 2, 3, 4];
let iterator1 = iterable[Symbol.iterator]();
let iterator2 = iterable[Symbol.iterator]();
iterator1.next(); // { value: 1, done: false }
iterator2.next(); // { value: 1, done: false }
iterator2.next(); // { value: 2, done: false }
iterator2.next(); // { value: 3, done: false }
iterator1.next(); // { value: 2, done: false }
//可以看到iterator1和iterator2是互不影响的
这个例子很勉强,重复遍历相同的数据事时上不太常见。但是异步处理迭代之间的每个值,这在Koa和co中就很好地表现了这一点。尽管它们是利用生成器函数返回的遍历器。
在最初的遍历器设计中,这个next
方法只会返回元素值。那么问题就来了:我怎么知道遍历器已经完成所有的遍历行为呢? 如果让next
函数抛出一个错误来标明遍历已完成这样又太土了!因为这样你在调用next
函数时,就必须给它包一层try/catch
。所以才会让next
函数返回的数据里有一个done
属性。
请看下面的代码:
let iterable = [1, 2];
let it = iterable[Symbol.iterator]();
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
//明明只有2个元素,为毛非要遍历三次,才算遍历完成,蛋疼????!!!!
it.next() // { value: undefined, done: true }
当next
方法返回的结果done
属性为true
时,遍历才算完成,并且value
属性的值为undefined
。遍历器让value和down:true
的对象为返回值,是为了让这个down:true
的对象作为一个遍历完成的标志,并不是一个遍历的元素。例如在for-of
,Array.from
的内部实现里,它们都会忽略掉这个返回值。
是的,这样是可以的。请看下面的例子:
let echoIterator = {
next(value) {
return { value, done: false };
}
};
echoIterator.next(42); // { value: 42, done: false }
正如上面的例子一样,你在自定义的遍历器里,是可以给next方法配置参数的!但是这种遍历器如果使用for-of
或Array.from
这些语法时,就会发生异常,因为for-of
在调用你的next方法时并不会传递参数给next方法。所以请慎用这种方式。
是的,你可以无限的调用遍历器的next
方法,即使遍历行为已经完成。只是返回的结果都是value=undefined,down=true的对象。
先来一个快速的知识回顾:
- 可变遍历接口要求实现一个
[Symbol.iterator]
方法; - 遍历器接口需要实现一个
next
方法;
请打开你的脑洞:要是我们让一个对象既有[Symbol.iterator]
方法,既有next
方法,那这个对象岂不是成了一个可遍历的遍历器了。
实际上,大多数遍历器都实现了可遍历接口,都有[Symbol.iterator]
方法,但是请记住:遍历器的[Symbol.iterator]
方法通常会返回遍历器本身,而不是一个新的遍历器对象。
请看下面的例子:
let iterable = [1, 2];
let iterator = iterable[Symbol.iterator]();
var iterator1 = iterable[Symbol.iterator]();
var iterator2 = iterable[Symbol.iterator]();
iterator1 == iterator2 // false
//为毛iterator1又不等于iterator2了? 亲,iterator1和iterator2是分别由Iterable Oject的`[Symbol.iterator]`方法生成的对象,它们当然不相等了。
var iterator3 = iterator[Symbol.iterator]();
var iterator4 = iterator[Symbol.iterator]();
iterator == iterator3 == iterator4 // true
//iterator3和iterator4都是指向的是iterator,iterator和iterator3和iterator4其实是一个对象所以它们是相等的。
这是一个简单的可遍历的遍历器:
let iterableIterator = {
next() {/*...*/},
[Symbol.iterator]() {
return this;
}
};
现在你可能会问:这样有什么用啊? 然并卵?
好的,现在请思考一个问题: 一个数据源可能需要有多次遍历行为.
请记住:for-of
语法的内部实现里,它会重复调用iterable对象的[Symbol.iterator]
方法来获取iterator.(我这这里其实有一个疑问:为毛for-of的内部实现里,会一直不断的获取iterator,既然iterator的[Symbol.iterator]
方法方法会返回iterator自身,那还需调用这个方法来获取iterator嘛!这个疑问估计得看真正的for-of的内部实现源码才能明白了)
通过给iterator对象添加[Symbol.iterator]
方法来返回iterator对象本自身,这样设计很重要的一点就是:通用
。
怎么解释这个通用呢?
假设现在有一个方法A,方法A接收的参数是一个Iterable Object(方法A是干啥的你先不要关心),但是你现在传递了一个iterator对象给方法A,试想一下如果iterotor对象里没有[Symbol.iterator]
方法,那方法A肯定不能顺利执行了。所以当给iterator对象添加[Symbol.iterator]
方法后,任何需要参数为Iterable object的函数,你传给这个函数一个iterator对象,这个函数还是能正常work。希望我这样解释通用
大家能明白。
let arr = ['a', 'b'];
let keysIterator = arr.keys(); // 获取遍历器
keysIterator[Symbol.iterator]() === keysIterator; // `keysIterator` 就是一个 Iterable iterator
//for-of会重复的调用`keysIterator[Symbol.iterator]()`,来获取`keysIterator`
//之后遍历它
for (let key of keysIterator) {
console.log(key); // 0, 1 (the array indexes)
}
因为iterator的[Symbol.iterator]
方法会返回iterator自身,所以我们在用的时候一定要小心这个问题,不然代码执行出错了就坑爹了。
请看下面的例子:
let iterable = [1, 2, 3, 4];
let iterator = iterable[Symbol.iterator]();
//先手动的遍历2次
iterator.next();
iterator.next();
//再调用for-of去遍历
for (let item of iterator) {
console.log(item); // 3, 4
//注意了这里只遍历了2次,并不会从头开始遍历
}
//如果你想从头开始遍历,你可以这样写
for (let item of iterable[Symbol.iterator]()) {
console.log(item); // 1, 2, 3, 4
}
原因就不在多说了,相信大家都懂的。
啊啊啊啊啊!谢谢你看完了这篇文章!希望我的介绍没问题,让你理解了iterables和iterators。
恩,文章差不多结束了,可能你有想继续学习ES6的内容,你可以看下,下一篇文章:Generators。
最后如果你想改善一下这篇文章,请点击这里:JS Rocks repository(额,这是英文原文),我们将会很高兴地与你讨论并接纳你的意见。
至于文章知识点总结就省略了,希望大家不要打我...
- ECMAScript® 2015 Language Specification - Ecma International
- for...of - MDN
- Iteration protocols - MDN
- for-of reimplementation - Babel
- Symbol - MDN
- Object initializer - MDN
- Method definitions - MDN
- Arrow functions - MDN
文章来自 [{{ site.url }}]({{ site.url }})