There have been a lot of back-and-forth exchanges between JavaScript developers on the efficacy of backwards looping versus the usual forward kind. While there are staunch proponents for both sides, trying to save a few cycles within a loop is neither here nor there, unless you are dealing with hundreds of thousands of items, and, if you are working with arrays that large, why the heck are you doing that in JavaScript?! The purpose of this tutorial is not to put every loop type to the test. Rather, I’d like to describe what kind of scenario might logically benefit from backwards iteration and how to recognize such a situation should you come across one in the future.
By the way, if you were wondering what kind of loop is generally the fastest in JavaScript, Greg Reimer, a Web Technologist for Sun.COM Web Design, compared a lot of loops in JavaScript, in a lot of browsers. In all cases the fastest loop was:
var i = arr.length; //or 10 while(i--) { //... }
He also has a test suite so you can run them yourself.
Where Looping Backwards is Easier
Notice that I said “easier,” and not faster. It may be faster, or it might turn out to be about the same speed, but what’s important is that looping backwards makes it both easier for us to wrap our brains around the process, while also requiring somewhat less code. As far as I’m concerned, these are both good things. Consider the law of Wile E. Coyote: “the more complex a process is, the higher the chance that it will blow up spectacularly!”
My personal foray into backwards iteration was borne of frustration. I was trying to split a flat array of item attributes into its constituent items. This task would have been fairly straightforward if each item has the same number of attributes. However, in the course of the application life cycle, a variable number of new attributes were appended to each item so that now the only to know where one item ended and the other began was that the first attribute was always a link to the product image.
Have a look at the following sample array and notice the position of those attributes that contain a path:
var menuData = [ '/wp-content/plugins/menu-plugin/images/menu-item.png', 'French Butter Croissant', '1.20/1.20', 'Our croissants have all the flavour and lightness of a classic French croissant.', 'menu_type_vegetarian', 'menu_allergen_gluten', 'menu_allergen_milk', 'menu_allergen_eggs', 'menu_allergen_soya_beans', '/wp-content/plugins/menu-plugin/images/menu-item.png', 'Pretzels', '1.25/1.50', 'There are three different types of our Pretzel (plain, poppy seeds or sesame).', 'menu_type_vegan', 'menu_type_vegetarian', 'menu_allergen_gluten', 'menu_allergen_sesame', '/wp-content/plugins/menu-plugin/images/menu-item.png', 'Free-Range Egg Mayo & Roasted Tomato Breakfast Baguette', '1.99/2.39', 'Chunky egg mayonnaise and roasted tomatoes are a great breakfast combination.', 'menu_type_vegetarian', 'menu_allergen_gluten', 'menu_allergen_eggs', 'menu_allergen_celery', 'menu_allergen_mustard', 'menu_allergen_sesame', '/wp-content/plugins/menu-plugin/images/menu-item.png', 'Five Berry Pot', '1.35/1.60', 'A simple little pot; very berry compote topped with natural yoghurt.', 'menu_type_vegetarian', 'menu_allergen_milk', ];
I had originally written my code to iterate in a forward direction, as per usual. Then, in testing, I discovered that the last item was not being copied to the new array. To fix that issue, I had to check for the end of the list in addition to the image attribute. To make a long story short, the whole thing was getting a little clunky. That’s when it dawned on me that if I were to reverse the loop, it would always finish on the image attribute, so the loop could exit normally without dropping elements.
Enter the Backwards Looping Function
Not only did the loop now finish on the delimiting element, but I was also able to easily remove items from the end of the array because Array functions like slice() and splice() automatically remove up to the end of the array if only the starting element index is provided.
I based my loop on the while ( --i ) {}
pattern so as to make the exit test as easy as possible.
Finally, I added an optional argument to remove each matched delimiter element in case I wanted to substitute another in its place.
Array.prototype.arraySplitReverse = function(pattern, removeMatchedElements){ var arr = this, retval = [], i = arr.length; while ( --i ) { if (typeof(arr[i]) === 'string' && pattern.test(arr[i])) { //remove all elements up till end of array var tmpArray = arr.splice(i); //remove first (matched) element from temp array if (removeMatchedElements === true) tmpArray.shift(); //add the temp array to the start of the new array //as a 2nd dimension retval.unshift(tmpArray); //reset i i = arr.length; } } return retval.length > 0 ? retval : arr; }
Calling the arraySplitReverse() Function
The one required argument is a RegEx for matching the delimiter elements. So, to apply the function to the above array, we would do the following:
var processedArray = menuData.arraySplitReverse(/^(https?:\/)?\/\S+\.(?:jpg|png|gif)$/i);
That produces an array consisting of three nested arrays of varying sizes:
(3) [Array(8), Array(10), Array(0), Array(6)] 0: (8) [ "/wp-content/plugins/menu-plugin/images/menu-item.png", "Pretzels", "1.25/1.50", "There are three different types of our Pretzel (plain, poppy seeds or sesame).", "menu_type_vegan", "menu_type_vegetarian", "menu_allergen_gluten", "menu_allergen_sesame" ] 1: (10) [ "/wp-content/plugins/menu-plugin/images/menu-item.png", "Free-Range Egg Mayo & Roasted Tomato Breakfast Baguette", "1.99/2.39", "Chunky egg mayonnaise and roasted tomatoes are a great breakfast combination.", "menu_type_vegetarian", "menu_allergen_gluten", "menu_allergen_eggs", "menu_allergen_celery", "menu_allergen_mustard", "menu_allergen_sesame"] 2: (6) [ "/wp-content/plugins/menu-plugin/images/menu-item.png", "Five Berry Pot", "1.35/1.60", "A simple little pot; very berry compote topped with natural yoghurt.", "menu_type_vegetarian", "menu_allergen_milk" ]
Removing the Matched Elements
As described above, the optional removeMatchedElements
argument should be a boolean value where true causes the function to remove matched elements.
Here is the invocation code and results:
var processedArray = menuData.arraySplitReverse(/^(https?:\/)?\/\S+\.(?:jpg|png|gif)$/i, true); //produces: (3) [Array(7), Array(9), Array(0), Array(5)] 0: (7) [ "Pretzels", "1.25/1.50", "There are three different types of our Pretzel (plain, poppy seeds or sesame).", "menu_type_vegan", "menu_type_vegetarian", "menu_allergen_gluten", "menu_allergen_sesame" ] 1: (9) [ "Free-Range Egg Mayo & Roasted Tomato Breakfast Baguette", "1.99/2.39", "Chunky egg mayonnaise and roasted tomatoes are a great breakfast combination.", "menu_type_vegetarian", "menu_allergen_gluten", "menu_allergen_eggs", "menu_allergen_celery", "menu_allergen_mustard", "menu_allergen_sesame" ] 2: (5) [ "Five Berry Pot", "1.35/1.60", "A simple little pot; very berry compote topped with natural yoghurt.", "menu_type_vegetarian", "menu_allergen_milk" ]
All of today’s code is up on Codepen for your viewing pleasure.
Conclusion
To everything there is a season and a time to every purpose…a time to gain, a time to lose, a time to rend, a time to sew, a time for love, a time for hate, a time for peace, and a time for backwards iteration. Remember, any time you come across a flat array with a variable number of elements, each starting with a recognizable pattern, it may be a candidate for the backwards looping treatment.