The .each() Side-car
Straps.js is micro-library that grew out of Bootstrap.js, a larger cross-platform library for client-sided scripting that I started working on in 2006. I have since abandoned this project in favour of jQuery, and Twitter has come along and unintentionally snatched the name, but the best parts of it now live on in Straps.js, which was heavily used to build the Paper.js project.
While the main focus of Straps.js is to provide clean and efficient structures for class inheritance with support for simple accessors and AOP patterns, I want to first write about a reoccurring coding pattern that I’ve started to really enjoy: A little trick that I call the .each() side-car, pronounced: dot-each side-car.
The Array#forEach(iterator, [bind])
method which was introduced in JavaScript 1.6 is widely known by now, and with it the second optional bind
argument, which is the object that becomes this
inside the iterator()
function when it is called for each element of the array.
Many libraries have defined similar methods for iterating over the properties of objects that are used as key → value stores, for examples Underscore.js’ _.each(object, iterator, [bind])
, which is very similar to Straps.js’ Base.each(object, iterator, [bind])
:
Both are capable of iterating over objects as well as arrays, and both implement the same binding behavior for the iterator function. But Base.each()
implements an additional detail that has proven very useful:
It returns the bind
argument at the end of the call. This allows for the inlined creation of the bind
argument on the function call, which becomes this
inside the iterator()
and is then returned from the the .each()
call at the end (which visually is at the top). Using this approach, I can for example very easily write functionality similar to Array#map()
:
- var result = Base.each(
- // The list over which to iterate
- ['one', 'two', 'three'],
- // The iterator function. Note the use of `this` inside it:
- function(word) {
- // Let's uppercase the words, to show that something has happened:
- this.push(word.toUpperCase());
- },
- // the bind object, which I call the side-car. It becomes `this` in the
- // above function.
- []
- );
- console.log(result); // 'ONE', 'TWO', 'THREE'
Try it out at sketch.paperjs.org
And while I could have achieved the same with Array#map()
, the side-car approach offers much more flexibility. Here a simple real-world example from Paper.js, where instead of using an array, the results are collected in an object, which is then returned and injected straight into the Point
class.
- Point.inject(Base.each(
- ['add', 'subtract', 'multiply', 'divide', 'modulo', 'negate'],
- function(name) {
- // Create an alias for each math method to be injected into the
- // classes using Straps.js' #inject()
- this['__' + name] = '#' + name;
- },
- {}
- ));
I have yet to write about Straps.js-style injection, but what basically happens here is that all properties of the object returned from Base.each()
are copied over to the prototype of the class, with some additional processing. For example, string values that start with '#'
are interpreted as aliases and are resolved on the prototype.
You will find this pattern all over the Paper.js library, and once you get used to the reversed reading direction (the bind
side-car argument becomes this
inside the iterator()
function and is returned from the .each()
call at the top), it is actually pretty easy to read what is happening in the code.
And here one final example: How to create a reverse lookup table, the side-car way:
- var frenchToEnglish = { un: 'one', deux: 'two', trois: 'three' };
- var englishToFrench = Base.each(frenchToEnglish, function(value, key) {
- this[value] = key;
- }, {});
- console.log(englishToFrench); // { one: 'un', two: 'deux', three: 'trois' }
Perhaps at first sight this does not look much better than the pure JS way, but the fact that it can all be written in one statement is a big advantage that allows for nesting and direct passing of the result to other functions. In Paper.js we take advantage of this pattern in many places.
blog comments powered by Disqus