Sorting Arrays with JavaScript iterators
A lot of front-end web development involves displaying sorted lists on a page and maintaining the state and logic of the list.
These sorted lists may be things like a list of products or an employee directory. Often, we will give the user the ability to re-sort the list or sort by a different key.
The other day, I was reading about well-known symbols and I realized that one of those symbols, @@iterator
could be used to make this sorting problem easier.
What are JavaScript Symbols?
Stepping back a little, a Symbol
is a primitive data type that is new to ES6. Each time you create a new Symbol
, it is guaranteed to be unique.
A well-known symbol is one of a set of predefined symbols. Well-known symbols are mostly used to determine how JavaScript operates on object prototypes. For example, some of the well-known symbols are used to determine how RegEx or concat operates on an object.
For simplicity, well-known symbols are commonly shortened from Symbol.symbolName
to simply @@symbolName
.
What can Symbol.iterator be used for?
@@iterator
is used to define an iteration function for an object. This function is used to provide a finite or infinite sequence of values to another JavaScript function.
The JavaScript functions that accept can accept a sequence from an iterator include:
- the
for...of
loop - the
...spread
operator - any custom function that understands the object returned by an iterable
How do you define an iterator?
An iterator is defined on the prototype of an object by declaring a function with a name of Symbol.iterator
. That function should return an object. The object must define a function called next
that returns an object. That object should have two values: value
(the value being returned by the iterator) and done
(a boolean that describes if we are at the end of the iterator or not). If done
is false (and we’re not at the end), then it can be omitted. If done
is true, then value
may be omitted. The general expectation is that if done
is true, then value
will be undefined but this can be different in some cases.
All of that is a bit hard to imagine, so let me show an example:
class SortedArray extends Array { constructor(arr, direction, key) { super(...arr); this.direction = direction; this.key = key; } [Symbol.iterator]() { let index = -1; if (!this.sorted) { this.sort(); } return { next: () => ({ value: this[++index], done: !(index in this) }) }; } }
My example shows a SortedArray
class that extends the native Array
object. The constructor of this class takes three arguments: an array of values, the direction of the sort, and the key that we are sorting on (the latter two are used by the sort function which is not shown here).
The second block of code defines our iterator.
[Symbol.iterator]
is the name definition of the function. The square brackets tell JavaScript that the name of the function is dynamic and will be determined at run time. For example, you can also use them to define functions with names such as ['function' + index]
, if you were creating functions in a loop.
As discussed above, the name, Symbol.iterator
, is the well-known symbol that tells JavaScript that this is the custom iterator for this object.
And as you can see, the iterator returns an object that contains a next function. This function,
() => ({ value: this[++index], done: !(index in this) })
happens to be an arrow function. The arrow function returns an object that contains a value
and a done
boolean.
So the code here is pretty simple. The main advantage here is that the object maintains a sorted
boolean that tracks whether or not the array is currently sorted. That way we are only doing the sort operation when something is trying to iterate over this object and the sort order is maintained.
If you look below to the full code example, you’ll see some other fun things. For example, there is no need for the developer using this object to directly manipulate the sorted
variable. All of the other variables use getters and setters to maintain access and those setters are used to correctly set the sorted
variable.
Special note: you’ll notice that the getters/setters and their values have different names. For example, the key
setter is used to set the _key
variable. Using the slightly different names prevents an infinite loop. If you had the same name, setting the variable would trigger the setter which would set the value which would trigger the setter which would set….you get the idea.
The full example is used for maintaining arrays of objects. So sorting can be done on either a numeric key or a string value. I’ve also implemented some other nice ES2015 features like parameter defaults and used the super
constructor to access the constructor of the Array
parent object.
You can find the full code example at:
https://github.com/gwmccull/es6-generator