有趣的迭代协议
什么是协议
我们遇到过很多协议:http协议,rpc协议,离婚协议等等,那么究竟什么是协议?首先,协议必定有两方,其次两方都要遵守一定的格式。接下来聊聊我们今天的主角--迭代协议。
使用迭代协议我们可以做什么
在 vue 模板的列表渲染中,有这样的语法:
<li v-for="item in list"/>
<li v-for="(item,idx) in list"/>
在迭代中获取一个 idx 很方便,但是在 js 的 for ... of... 循环中,我们可以拿到数组中的一项,但是得到索引却要费一番功夫。而传统的基于索引的循环,则要多写一些代码。那么有没有方法,让我们在 for... of 循环中,既能拿到数组的项,又能拿到索引呢?如下所示:
for (let [item, idx] of list) { ... }
答案是肯定的,就是要借助今天的主角,迭代协议了。
迭代器协议和可迭代对象
迭代器协议(iterator protocol) 定义了一种标准的方式来产生一个有限或无限序列的值,并且当所有的值都已经被迭代后,就会有一个默认的返回值。当一个对象只有满足下述条件才会被认为是一个迭代器,它实现了一个 next() 的方法,而 next 方法会返回这样一个对象:{value, done},value 表示本次迭代的值,done 表示迭代是否结束。根据这个协议,我们可以写一个迭代器:
let iterator = {
from: 1,
to: 10,
next() {
if (this.from < 10) {
return {done: false, value: this.from++}
}
return {done: true}
}
}
于是我们可以手动调用 next()来迭代:
iterator.next()
// {done: false, value: 1}
iterator.next()
// {done: false, value: 2}
iterator.next()
// {done: false, value: 3}
或者使用 while 循环来迭代:
while(true) {
let next = iterator.next()
if(next.done) break // 迭代结束
console.log(next.value)
}
上面的代码尽管实现了迭代,却不能使用 for .. of .. 或者 [...]
这样的内置语法来迭代。
for (let i of iterator) { console.log(i) } // Uncaught TypeError: iterator is not iterable
要实现更自然的迭代方式,我们还需要了解可迭代对象协议:** 为了变成可迭代对象, 一个对象必须实现 @@iterator 方法, 意思是这个对象(或者它原型链 prototype chain 上的某个对象)必须有一个名字是 Symbol.iterator 的方法 **
这个也很好理解,因为 for...of 这样的语法是为可迭代对象设计的。那么什么是可迭代对象呢?打个比方,假如你想 35 岁以后跑滴滴,那么首先你必须有一辆车,这时你就被称为 可跑滴滴的人
。同理,如果一个对象有了 Symbol.iterator 的方法,而且这个方法调用后会返回一个迭代器,这时,这个对象就成了一个可迭代对象。下面我们来看看怎么使一个普通对象变成可迭代的对象。
// 普通对象
let obj = {
a: 'x',
b: 'y',
c: 'z'
}
实现 Symbol.iterator 就可以将普通对象变成可迭代对象:
let obj = {
a: 'x',
b: 'y',
c: 'z'
}
obj[Symbol.iterator] = function() {
let _keys = Object.keys(this)
let _idx = 0
return {
next() {
let key = _keys[_idx++]
if (!key) {
return {done: true}
}
return {
value: [key, this[key]],
done: false
}
}
}
}
这时就可以使用 for...of... 来遍历 obj 了😄:
for (let [k, v] of obj) {console.log(k, v)}
了解了可迭代对象,我们就可以使前面的 iterator 变成一个可迭代对象,然后用 for...of 来迭代了:
iterator[Symbol.iterator] = function() { return this }
由于 iterator 本身就是一个迭代器,使它成为一个可迭代对象只需要为它增加一个方法,这个方法什么也不需要做,只需要 return this 即可。另外,这也不是我们的创新,因为 String.prototype.matchAll 已经这么做了:
iter = '12345678'.matchAll(/\d/g)
iter.next() // {value: Array(1), done: false} 说明 iter 是一个迭代器
[...iter] // 没有报错,说明 iter 也是一个可迭代对象
生成器函数
如果你觉得为了要使一个对象变的可迭代,要自己去理清楚什么 next,done 很麻烦,那么生成器函数就是为你准备的。因为它会返回一个对象,** 同时实现了迭代器协议和可迭代对象协议 **:
function* generator() {
yield 1;
}
let iter = generator();
iter.next() // {value: 1, done: false}
iter.next() // {value: undefined, done: true}
你一样可以使用 for...of... 来迭代 iter:
let iter = generator();
for (let [k, v] of iter) { console.log(k, v) }
内置的迭代对象
我们知道可以使用 for...of 来迭代数组,字符串这些对象,那是因为 String, Array, TypedArray, Map, Set 都是可迭代对象,他们的原型都实现了 Symbol.iterator 方法。文章一开始,我们希望对一个数组迭代时,除了拿到它的值以外,还可以拿到他的索引,那我们就需要对 [Symbol.iterator] 方法进行重载修改, 我们可以实现一个 reload 方法来修改数组的 Symbol.iterator 方法:
function reload(arr) {
if (Array.isArray(arr)) {
arr[Symbol.iterator] = function*() {
for (let i = 0; i < this.length; i++) {
yield [this[i], i]
}
}
}
}
var arr = ['x', 'y', 'z']
reload(arr)
for (let [item, idx] of arr) {
console.log(item, idx)
}
上面的实现方法会破坏数组的原本性质,这无异于给接管你代码的人埋坑。我们可以采用更温和的方法, 通过一个生成器函数来实现:
function* withIndex (arr) {
let i = 0;
while(i < arr.length - 1) {
yield [arr[i], i];
i++;
}
}
var arr = ['x', 'y', 'z'];
for (let [v, i] of withIndex(arr)) {
console.log(v, i);
}
最后一个问题:为什么不直接修改 Array.prototype[Symbol.iterator]方法呢?因为解构也用到了迭代协议,那不是我们想要的结果😊。有兴趣的同学可以试试直接修改原型上的方法。
上面讲的全部是同步迭代的内容,随着ES的发展,后来又出现了一步迭代,迭代协议和promise又能擦出怎样的火花呢,可以看这篇:https://2ality.com/2016/10/asynchronous-iteration.html
武汉加油!本文完。