JS可迭代对象
czpcalm 人气:01、迭代器
迭代器是借鉴C++等语言的概念,迭代器的原理就像指针一样,它指向数据集合中的某个元素,你可以获取它指向的元素,也可以移动它以获取其它元素。迭代器类似于数组中下标的拓展,各种数据结构,如链表(List)、集合(Set)、映射(Map)都有与之对应的迭代器。
JS中的迭代器是专门为了遍历这一操作设计的。每次获取到的迭代器总是初始指向第一个元素,并且迭代器只有next()
一种行为,直到获取到数据集的最后一个元素。我们无法灵活移动迭代器的位置,所以,迭代器的任务,是按某种次序遍历数据集中的元素。
JS规定,迭代器必须实现next()
接口,它应该返回当前元素并将迭代器指向下一个元素,返回的对象格式为{value:元素值, done:是否遍历结束}
,其中,done是一个布尔值。done
属性为true的时候,我们默认不会去读取value
, 所以最后返回的经常是{value: undifined, done: true}
,注意,返回类似{value: 2, done: true}
不会导致错误,但是因为done
设置为true,在for...of
等操作中都会忽略value
的值。因此,done:false
和value:undifined
可以省略。一个简单的JS迭代器像这样:
let iter = { i: 0, next() { if (this.i > 10) return { done: true }; return { value: this.i++ }; } } //手动使用迭代器 console.log(iter.next()); //{ value: 0 } console.log(iter.next()); //{ value: 1 } while (true) { let item = iter.next(); if (!item.done) { console.log(item.value); //打印从2到10 } else { break; } }
可以看到,迭代器与普通的JS对象没有区别,它就是一个用于实现迭代的对象。手动操作迭代器并没有什么实用性,迭代器的作用是附着在对象上,让一个对象,或者数据结构成为可迭代对象。
2、迭代器接口与可迭代对象
迭代器接口是我们获取对象迭代器时默认调用的接口,一个实现了迭代接口的对象即是可迭代对象。JS的默认迭代接口是[Symbol.iterator]
, 一个对象实现了[Symbol.iterator]
接口就成为了可迭代对象。
[Symbol.iterator]
是一个特殊的Symbol
属性,它用于JS内部检测一个对象是否为可迭代对象。接口一词的含义代表它是一个函数,其结果应该放回一个迭代器。结合上面迭代器必须要有next()
操作,所以,对可迭代对象,调用链iterableObj[Symbol.iterator]().next()
应该是可行的。数组是最具代表性的可迭代对象,让我们拿数组测试一下:
arr = [1, '2', {a: 3}]; let arrIt = arr[Symbol.iterator](); //获取数组迭代器 console.log(arrIt.next()); //{ value: 1, done: false } console.log(arrIt.next()); //{ value: '2', done: false } console.log(arrIt.next()); //{ value: { a: 3 }, done: false } console.log(arrIt.next()); //{ value: undefined, done: true }
可以看到,迭代器的next()
接口确实如愿工作,并且返回上述的结构。
3、自定义可迭代对象
现在,让我们来实现几个可迭代对象,这十分简单,只要:
- 实现对象的迭代器接口
[Symbol.iterator]()
,注意它是一个方法, - 在迭代器接口中返回一个迭代器对象,
- 确保迭代器对象具有
next()
接口,并且返回{value: v, done: bool}
的结构。
3.1、可迭代的Range对象
作为第一个可迭代对象,我们来实现类似python中的range(from, to)
,不过这里使用Range
对象来封装一个左闭右开的范围[from, to)
。
function Range(from, to) { this.from = from; this.to = to; } Range.prototype[Symbol.iterator] = function () { //返回一个迭代器对象 return { cur: this.from, to: this.to, //保证next()中可以获取 next() { return (this.cur < this.to) ? { value: this.cur++, done: false } : { value: undefined, done: true }; } } } let range = new Range(5, 11); //创建一个range对象 //使用for...of循环 for (const num of range) { console.log(num); //依次打印5,6,7,8,9,10 } //使用 let arrFromRange = Array.from(range); console.log(arrFromRange); //[5,6,7,8,9,10]
3.2、使用Generator函数作为迭代器接口
因为Generator函数产生的generator对象是一种特殊的迭代器,所以我们可以很方法地使用Generator函数作为对象的迭代器接口。使用Generator函数改写上面的迭代器接口:
Range.prototype[Symbol.iterator] = function* () { for (let i = this.from; i < this.to; i++) { yield i; } }
这种写法更加简洁易懂,是最为推荐的写法,Generator函数中产生的值就是遍历过程中得到的值。
3.3、可迭代的List
接下来,我们自定义一个链表节点List
,在此我们省去不必要的接口。
function ListNode(value) { this.value = value; this.nextNode = null; } function List(root) { this.cur = this.root = root; } //List的next接口 List.prototype.next = function () { if (this.cur) { //非尾哨兵节点 let curNode = { value: this.cur.value }; this.cur = this.cur.nextNode; return curNode; } else { return { done: true }; } }
List.next()
实现了将链表指针后移的操纵,并且返回了移动前节点的值,你可能注意到,我特意让返回的对象格式与迭代器返回结果一致,下面你将看到这么做的原因。现在我们让List
变成可迭代,按照之前的做法,使得List[Symbol.iterator]().next()
能够返回正确的{value: v, done: true}
格式。是的,我们已经画好龙了,就差一个点睛之笔:
List.prototype[Symbol.iterator] = function () { return this; }
随手写一个测试:
let a = new ListNode('a'), b = new ListNode('b'), c = new ListNode('c'); a.nextNode = b, b.nextNode = c; let list = new List(a); for (let i of list) { console.log(i); //a,b,c }
Perfect! List
的迭代器接口返回了它自己,利用了自身的next()
接口完成迭代操作,也就是说List
的迭代器是List
本身,我都为自己构思的例子觉得巧妙。
3.3、可迭代的迭代器
上面的List
例子会让人觉得有点牵强,list.next()
的返回值为了迎合迭代器的要求,让平时不得不使用let curValue = list.next().value
来正确接收返回的节点值,确实。但是,这种做法在一种时候让人觉得眼前一亮——让迭代器称为可迭代对象,因为自己就是可迭代器,让自己成为自己的迭代器,就像1=1
一样正确自然。
回到开头埋下的雷,我们只需要稍加改动
let iter = { i: 0, next() { if (this.i > 10) return { done: true }; return { value: this.i++ }; }, //让迭代器的迭代器接口返回自身 [Symbol.iterator]() { return this; } } //这样,你就可以把迭代器用在任何可迭代对象的地方 for (let i of iter) { console.log(i); }
这样,这个迭代器本身也是可迭代的。需要注意的是,内置可迭代类型的迭代器也都是可迭代的,类似for(let i of arr[Symbol.iterator]())
的操作是可行,其原理是让Array的迭代器继承Array.prototype
。其它类型也有类似的继承,如Generator与generator对象。
4、可迭代对象的意义
可迭代对象作为数组的扩充,具有非凡的意义。在以前,对一个需要操作一组数据的接口,只有数组这种结构能支持,非数组对象必须通过某种方式转化为数组,完成之后,还可能需要还原成原来的结构,这种繁琐的来回切换很不理想。有了可迭代对象的概念,这类操作可以接受一个可迭代对象,数组是可迭代对象,所以之前的数组参数是仍然可行的,在此之上,任何实现了可迭代接口的对象,也能正常处理。考虑这个下面例子:
function sumInArray(arr){ let sum=0; for(let i = 0;i<arr.length;i++){ sum+=arr[i]; } return sum; } function sumInIterable(iterable){ let sum = 0; for(let num of iterable){ sum+=num; } return sum; }
sumInArray()
只对数组友好,而sumInIterable()
是所有可迭代对象通用的,例如我们前面的Range
对象,iter
对象。是的,数组到可迭代对象的提升,代表了接口的通用性的提升。这个道理太浅显易懂,以至于你可能觉得我说废话,那么,请问你在接口设计的时候,会考虑能否使用可迭代对象代替数组吗?个人认为这种提升很多时候是有益的,特别在一些应用场景较多的接口,我发现很多ES6操作也是基于可迭代对象。如果有什么看法,也欢迎评论区探讨。
5、使用可迭代对象
先认识JS内建的可迭代对象:
非weak的数据结构
,包括Array
,Set
,Map
。DOM中的NodeList对象
。String对象
。函数的arguments属性
。
再了解哪些操作是基于可迭代对象的:
for...of
语法...iterable
:展开语法和解构赋值yield*
语法Map
,Set
,WeakMap
,WeakSet
的构造器。为什么没有Array?因为Array直接把它对象当成元素了,但是有Array.from(iterable)
。Object.fromEntries(iterable)
,每次迭代结果应该是一个对应键值对的二元数组,与Map
的迭代结果吻合,常有let obj = Object.fromEntries(map)
实现从map到object的转化。promise.all(iterable)
和promist.race(iterable)
.
我认为对这些方法的具体使用不该放在这里,如果使用过它们,自然了解,只需要记住它们对任何可迭代对象都是支持的。如果不认识它们我也说不完,你应该一一学习去。
6、后记
加载全部内容