Reputation: 1962
I'm implementing a doubly linked list as part of a programming exercise, and I would like to allow the developer to iterate through its nodes both forward and backwards using the for...in
notation.
At its most basic, the data structure looks like this:
class DoublyLinkedList {
constructor(data) {
if (data) {
this.head = new DoublyLinkedListNode(data)
} else {
this.head = null
}
}
append = (data) => {
if (!this.head) {
this.prepend(data)
} else {
const newTail = new DoublyLinkedListNode(data)
let current = this.head
while(current.next) {
current = current.next
}
current.next = newTail
newTail.prev = current
}
}
}
Next I added the generator functions:
*values() {
let current = this.head
while (current) {
yield current.data;
current = current.next;
}
}
*valuesBackward() {
let currentForwards = this.head
while (currentForwards.next) {
currentForwards = currentForwards.next
}
const tail = currentForwards
let currentBackwards = tail
while (currentBackwards) {
yield currentBackwards.data
currentBackwards = currentBackwards.prev
}
}
I'm able to add a single, forwards iterator with the following added to the class:
[Symbol.iterator]() { return this.values()}
I tried adding both of the following to the class:
iterateForward = () => [Symbol.iterator] = () => this.valuesBackward()
iterateBackward = () => [Symbol.iterator] = () => this.valuesBackward()
And then tried to iterate using for (node in list.iterateForward())
but this failed with error TypeError: undefined is not a function
.
I guess that made sense looking at the code so next I tried:
iterateForward = () => {
const vals = this.values()
const it = {
[Symbol.iterator]() {
return vals()
}
}
return it
}
This didn't error, but the iteration didn't work - the iterator ran zero times.
What am I missing here? Is it possible to achieve what I want?
Upvotes: 3
Views: 1048
Reputation: 707436
This stuff regularly confuses me so here's a summary we can both refer to.
Here's the background
An iterable
is an object that has the [Symbol.iterator]
property and then you when you call that property as a function, it returns an iterator object.
The iterator object has a property .next()
and each time you call that function, it returns the object with the expected properties {value: x, done: false}
. The iterator object will typically keep the state of the iteration in this separate object (thus iterators can be independent of each other).
So to support multiple iterators, you create multiple methods where each method returns an object that is a different iterable, each has it's own [Symbol.iterator]
that when called returns a different iterator object.
So, to recap, you:
[Symbol.iterator]
property on it and has access to the original object's data.[Symbol.iterator]
property, you get an iterator object..next()
method that gets you each item in the sequence and it does that by returning an object like this {value: x, done: false}
each time you call .next()
.You can skip step 1 and just have your core object have the [Symbol.iterator]
property on it. That essentially becomes your default iteration. If you do:
for (let x of myObj) {
console.log(x);
}
it will access myObj[Symbol.iterator]()
to get the iterator. But, if you want to have more than one way to iterate your collection, then you create separate functions that each return their own iterable (their own object with their own [Symbol.iterator]
property on them).
In an array, you've got .entries()
and .values()
as an example of two methods that return different iterables, that make different iterators.
let x = ['a', 'b', 'c'];
for (let v of x.values()) {
console.log(v);
}
This gives output:
'a'
'b'
'c'
Or, for .entries()
:
let x = ['a', 'b', 'c'];
for (let v of x.entries()) {
console.log(v);
}
[0, "a"]
[1, "b"]
[2, "c"]
So, each of .values()
and .entries()
returns a different object that each have a different [Symbol.iterator]
that when called as a function returns a different iterator function for their unique sequence.
And, in the case of the array, .values()
returns a function that when called give you the exact same iterator as just iterating the array directly (e.g. the [Symbol.iterator]
property on the array itself).
Now, for your specific situation
You want to create two methods, let's say .forward()
and .backward()
that each create an object with a [Symbol.iterator]
property that is a function that when called return their unique iterator object.
So, obj.forward()
would return an object with a [Symbol.iterator]
property that is a function that when called returns the iterator object with the appropriate .next()
property to iterate forward and the appropriate starting state.
So, obj.backward()
would return an object with a [Symbol.iterator]
property that is a function that when called returns the iterator object with the appropriate .next()
property to iterate backward and the appropriate starting state.
Here's an example using an array:
class myArray extends Array {
forward() {
return this; // core object already has appropriate forward
// [Symbol.iterator] property, so we can use it
}
backward() {
return {
[Symbol.iterator]: () => {
let i = this.length - 1; // maintains state in closure
return {
next: () => { // get next item in iteration
if (i < 0) {
return {done: true};
} else {
return {value: this[i--], done: false};
}
}
}
}
}
}
}
let x = new myArray('a', 'b', 'c');
console.log("Forward using default iterator")
for (let v of x) {
console.log(v);
}
console.log("\nUsing .forward()")
for (let v of x.forward()) {
console.log(v);
}
console.log("\nUsing .backward()")
for (let v of x.backward()) {
console.log(v);
}
Upvotes: 5