In my current project I have objects of the following structure:
CanvasBundle - Object
- Some elements...
gridCanvas
axesCanvas
functions[]
(contains unknown number offunctionCanvases
)overlayCanvas
I'd like to write an iterator which would iterative over all the canvases (grid
/axes
/func
/func1
/func2
/.../overlay
).
I've currently implemented it like this:
function makeCanvasBundleIterator(canvasBundle) {
var nextIndex = 0;
var functionIndex = 0;
return {
next: function() {
var value;
switch (nextIndex) {
case 0:
value = canvasBundle.grid;
break;
case 1:
value = canvasBundle.axes;
break;
case 2:
value = canvasBundle.functions[functionIndex];
break;
case 3:
value = canvasBundle.overlay;
break;
default:
break;
}
if (nextIndex != 2 || functionIndex == canvasBundle.functions.length -1) {
nextIndex++;
} else {
functionIndex++
}
return nextIndex <= 4 ?
{value: value, done: false} :
{done: true};
}
};
}
This allows me to do the following:
var it = makeCanvasBundleIterator(canvasBundle);
var next;
while (!(next = it.next()).done) {
console.log(next.value);
}
Coming from C, this code does not look clean at all. Since I'm a real JS beginner, I'd like a review with suggestions/advice how I can clean it up. Maybe there even is a completely different/better approach.
3 Answers 3
If you're implementing the iteration protocol - use a generator:
function* makeCanvasBundleIterator(canvasBundle) { // note the *
yield canvasBundle.grid;
yield canvasBundle.axes;
yield* canvasBundle.functions; // note the * for inner yield
yield canvasBundle.overlay;
}
And that's your entire function. Generators give us a declarative way to deal with the iteration protocol.
Now, you can also consume the result better - this is regardless if you want to use the generator or your own solution:
for(const value of makeCanvasBundleIterator(...)) {
// access value here
}
If you'd like to take a more OOP approach and signal that a CanvasBundle
is an iterable, you can implement the iterable protocol with Symbol.iterator
:
CanvasBundle.prototype[Symbol.iterator] = function*() {
yield canvasBundle.grid;
yield canvasBundle.axes;
yield* canvasBundle.functions; // note the * for inner yield
yield canvasBundle.overlay;
}
Which would allow you to iterate the canvas bundle itself:
for(const item of canvasBundle) {
// iterate item
}
Array.from(canvasBundle); // get iteration result as array
[...canvasBundle]; // also works and converts to an array.
And use it anywhere you'd use a regular iterator.
-
\$\begingroup\$ @LastSecondsToLive this is what generators do - their whole purpose is to declaratively create iterations. Note that unlike the other answer this code does not create an extra copy of the array at any point, no closures are involved and is efficient. Engines will probably (might) not even allocate a
{done, value}
since under the hood they know that it'll be consumed with afor... of
loop. \$\endgroup\$Benjamin Gruenbaum– Benjamin Gruenbaum2016年03月08日 08:24:40 +00:00Commented Mar 8, 2016 at 8:24 -
\$\begingroup\$ Great, awesome explanation :) Can I also add it to
CanvasBundle.prototype
? I'll probably go with it. \$\endgroup\$LastSecondsToLive– LastSecondsToLive2016年03月08日 08:51:58 +00:00Commented Mar 8, 2016 at 8:51 -
\$\begingroup\$ Yes, I'll edit it into my answer. \$\endgroup\$Benjamin Gruenbaum– Benjamin Gruenbaum2016年03月08日 08:52:28 +00:00Commented Mar 8, 2016 at 8:52
The switch
is awkward, and the nextIndex != 2
special case is cumbersome. It would be a bit simpler if you regularized everything:
function makeCanvasBundleIterator(canvasBundle) {
var i = 0, j = 0;
var items = [
[canvasBundle.grid],
[canvasBundle.axes],
canvasBundle.functions,
[canvasBundle.overlay],
};
return {
next: function() {
if (i >= items.length) {
return {done: true};
}
if (j >= items[i].length) {
i++;
j = 0;
}
return {done: false, value: items[i][j++]};
};
}
But that could be simplified further by linearizing the list up front using Array.concat()
:
function makeCanvasBundleIterator(canvasBundle) {
var i = 0;
var items = [].concat(
canvasBundle.grid,
canvasBundle.axes,
canvasBundle.functions,
canvasBundle.overlay
);
return {
next: function() {
return (i >= items.length) ? {done: true}
: {done: false, value: items[i++]};
};
}
For that matter, why bother making an iterator at all? Just return the linearized array. The caller can iterate through it conventionally using a for
loop or Array.forEach()
.
Note that the behaviour of these alternate solutions is different from the original if the canvasBundle
is modified during iteration. (You wouldn't want to do that, I hope?)
-
\$\begingroup\$ Exactly what I was looking for. Thank you once again - you did already review some of my C projects :) I don't want to modify the content of the
canvasBundle
. I'll probably add a function getCanvases to thecanvasBundle
-prototype, which returns the linearized array. \$\endgroup\$LastSecondsToLive– LastSecondsToLive2016年03月08日 00:57:55 +00:00Commented Mar 8, 2016 at 0:57
I made a library a while back when I first learned JavaScript to make it look similar to iterators in other languages. Here's the basic structure:
This creates a nullptr object to assist the library.
(function (global, defprop, freeze) {
function nullptr() {
defprop(this, "valueOf", { value: function() { return "nullptr" } });
defprop(this, "toString", { value: function() { return "nullptr" } });
}
defprop(global, "nullptr", { value: freeze(new nullptr()) });
})(this, Object.defineProperty, Object.freeze)
Now time for the actual iterator:
(function (global, defprop, np) {
function to_string(o) { return (o instanceof Object) ? o.toString() : o+"";};
function iterator(scope, props) {
// This line is not needed but is useful if want to make iterators like: var iter = iterator(scope, props);
if(!(this instanceof iterator)) return new iterator(scope, props);
var self = this,
_scope = scope,
_props = props,
_i = 0;
defprop(self, "valueOf", { value: function() { return _i; } });
defprop(self, "prop", { get: function() { return (0 <= _i && _i < _props.length) ? to_string(_props[_i]) : np; } });
defprop(self, "$", {
get: function() { return (0 <= _i && _i < _props.length) ? _scope[_props[_i]] : np; },
set: function(v) { if(0 <= _i && _i < _props.length) _scope[_props[_i]] = v; }
});
defprop(self, "me", {
get: function() { return self; },
set: function(v) { if(!isNaN(v = +v)) _i = v }
});
}
defprop(global, "iterator", { value: iterator });
})(this, Object.defineProperty, nullptr)
The use of this class is similarish to other iterators.
var scope = { a:0, b:1, c:2, d:3 };
for(var iter = new iterator(scope, ["a","d","c"]); iter.prop != nullptr; ++iter.me)
console.log(iter.prop + " : " + iter.$);
console.log(iter.prop + " : " + iter.$);
/* === output ===
a : 0
d : 3
c : 2
nullptr : nullptr
*/
I have it set up to where $
represents dereferencing. Also, everything returns nullptr
if out of bounds. The only different one is me
which returns itself. The reason for picking me
is because it's shorter than all of the others that refer to itself (besides I
...). By making it to where the valueOf
returns _i
allows for the ++
.
Can do more than just increment...
iter.me = 0; // Resets back to zero.
iter.me += 2; // Moved over two...
console.log(+iter); // => 2
iter.me -= 4;
console.log(+iter); // => -2
Can also add a toString
to iterator
but this will only be called when toString
is called not when adding a string to it because valueOf
is called first when adding.
defprop(self, "toString", { value: function() { return self.prop + " -> " + self.$ } })
You can add a next
or prev
if you want which I would make like:
defprop(self, "next", { value: function() { self.me++; return self.$ } });
defprop(self, "prev", { value: function() { self.me--; return self.$ } });
It's not very elegant but I figured if someone wanted an iterator that looked like an iterator and worked in more browsers, then this might help.
gridCanvas
,axesCanvas
, etc.) corresponds to an HTML-canvas element. These HTML-canvases all lay on top of each other (overlapping) and together they create a full picture. All these canvases are necessary to draw multiple layers, etc. The complete picture is stored in one of theCanvasBundles
shown above. When I need to redraw a whole picture, I need to iterate over all the contained canvases (to redraw every single one of them). The code above does the job, but I just think the code looks kind of dirty. \$\endgroup\$