The For-loop is Dead; Long Live the For-loop
A Brief History of Ugly Code
I began learning Javascript for the first time nearly 8 years ago (and then proceeded to not use any of it in the interceding 8 years.) It was a simpler time: You were encouraged to declare variables using an incredibly obvious reserved word called ‘var’; all lines of code ended in semicolons; and, despite the language having been around for 15 years, the early-career professional web developers I briefly studied alongside were just venturing into Javascript for the first time.
That’s right! In 2012, you could land and hold a job as a front-end web developer without having touched Javascript.
As I try to relearn and advance my understanding, I pulled out some of the old code I wrote back then. While it mostly reminded me of how much I’d forgotten, my second observation was…
My Lord, that’s a lot of for-loops!
Reading, manipulating, and looping through arrays is one of the most important skills for Javascript developers to master. Fortunately, a number of tools are available to early developers to master and memorize. The for-loop is just the most boring one.
ForEach
The for-loop the classic iterator. No need for a method call. Just a lot of ugly syntax that is completely unreadable to the uninitiated. The other methods I’ll mention here can all be distilled down to a for-loop as well, but those more complex methods are far more readable when called with their named methods. Still, the for loop remains the unchallenged champion for logging consecutive numbers into a console:
function counting() {
let str = "";
for (let i = 5; i < 11; i++) {
str = str += i;
}
return str;
}
counting()
// returns "5678910"
So simple, so boring, so confusing for someone who has never read a line of code.
The most similar method would be the ForEach, which quite simply, like a for loop, executes a function for each element in an array, returning nothing.
const arr = [5, 6, 7, 8, 9, 10];
function countingWithForEach(array) {
let str = "";
array.forEach (number => str += `${number}`);
return str;
}
countingWithForEach(arr)
// returns "5678910"
Admittedly, this is not much of an improvement on the classic for-loop. We avoid the need to declare an index position, but the code is much the same. The real value of iterators is found in the more transformational methods.
Map
The first iterator of interest is map, most useful when we are given an array of elements we wish to transform into an array of the same length, but with edited values. For example, we might have a list of names entered with all lowercase letters, but we wish to capitalize them. As a for loop, our map method to fix our array would look something like:
let beatles = ['john', 'paul', 'george', 'pete']
let capitalizeName = name => name.charAt(0).toUpperCase() + name.slice(1)
function capAllNames(names) {
let capNames = new Array;
for (i = 0; i < names.length; i++) {
capNames[i] = capitalizeName(names[i]);
}
return capNames;
}
capAllNames(beatles)
// returns ["John", "Paul", "George", "Pete"]
Boy is that ugly, but it gets the job done! Of course, there's one other problem... While we appreciate 'pete's' time in our beatles array, he really does not fit it. We need to take him out and put 'ringo' in, then capitalize again.
let beatles = ['john', 'paul', 'george', 'pete']
delete beatles[3]
beatles.push('ringo')
let capitalizeName = name => name.charAt(0).toUpperCase() + name.slice(1)
function capAllNames(names) {
let capNames = new Array;
for (i = 0; i < names.length; i++) {
capNames[i] = capitalizeName(names[i]);
}
return capNames;
}
capAllNames(beatles)
// returns Uncaught TypeError: Cannot read property 'charAt' of undefined
Hmmm... our simple for-loop has a problem with the empty slot in our array left by kicking 'pete' out. It's a edge-case, for sure, but it would be great if the capAllNames function could adjust for this without having to add a series of conditional statments to our block.
Enter the map iterator method:
let beatles = ['john', 'paul', 'george', 'pete']
delete beatles[3]
beatles.push('ringo')
let capitalizeName = name => name.charAt(0).toUpperCase() + name.slice(1)
beatles.map (name => capitalizeName(name));
// returns ["John", "Paul", "George", empty, "Ringo"]
We distilled our function to an easy one-liner for the map call. The map method also kindly passes over empty values of the array rather than give us an error. That’s great, since it allows our applications to continue without breaking if some object or data has been removed from our arrays. If we were limited to for-loops, we would have had to either add conditional statements that would make our code longer or research ways to remove empty values from our data arrays. With the map method, we can just...
Reduce
Another convenient iterator is JavaScript’s reduce method. This method allows us to reduce a number of values down to one. If we were to write a reduce method as a for-loop:
let renaissanceArtists = ['splinter', 'leonardo', 'raphael', 'michelangelo', 'donatello']
function longestName(names) {
let longName = new String;
for (let i = 0; i < names.length; i++) {
longName = longName.length > names[i].length ? longName : names[i];
}
return longName;
}
longestName(renaissanceArtists);
// returns 'michelangelo'
Excellent. Exactly, what we expected. A bit of a long route to get there, having to take time to initialize the loop function, the default empty string, and the index for our loop, in addition to the array. Upon closer look, someone put a 'splinter' in our list of Italian Renaissance Artists, and I don't think recall any Splinters being mentioned in art history class. Let's remove him and run it again.
let renaissanceArtists = ['splinter', 'leonardo', 'raphael', 'michelangelo', 'donatello']
delete renaissanceArtists[0]
function longestName(names) {
let longName = new String;
for (let i = 0; i < names.length; i++) {
longName = longName.length > names[i].length ? longName : names[i];
}
return longName;
}
longestName(renaissanceArtists);
// returns Uncaught TypeError: Cannot read property 'length' of undefined
That was disappointing. Our reduction for-loop can't read the string length for the empty element at renaissanceArtists[0]. In old Javascript, we would need to add so many conditional statements to account for every edge case in order for our function to work every time we call upon it, or painstakenly go through our arrays to make sure our data meets a standard. That seems like way too much work.
Could Javascript’s built-in .reduce method help?
let renaissanceArtists = ['splinter', 'leonardo', 'raphael', 'michelangelo', 'donatello']
delete renaissanceArtists[0]
renaissanceArtists.reduce( (longest, name) => longest.length > name.length ? longest : name );
// returns 'michelangelo'
Much better. We don't even need to write a single '{' or '}' nor do we take time to declare an initial variable.
Find
Another means of turning an array of elements into one return value would be Javascript's .find method. Whereas .reduce will have the elements of an array interact with each other to return a final value, the .find method will return an untransformed element (unless the method is called in combination with some other method or callback function).
Trying out the for-loop method first, we want to return a select element based on our query value.
let suspects = [{name: 'Philip Gerard', possessedBy: 'MIKE'}, {name: 'Leo Johnson'}, {name: 'Benjamin Horne', possessedBy: 'greed'}, {name: 'Log', possessedBy: 'Log Lady’s late husband'}, {name: 'Jacques Renault'}, {name: 'Leland Palmer', possessedBy: 'BOB'}];
function whoKilledLaura(suspects, evilSpirit) {
for (i = 0; i < suspects.length; i++) {
if (suspects[i].possessedBy === evilSpirit) {
return suspects[i].name;
}
}
}
whoKilledLaura(suspects, 'BOB');
// returns 'Leland Palmer'
Nesting an if statement inside our loop? Yuck.
Unlike our earlier for-loops, our funcation, or more specifically, the 'if' conditional, can handle empty objects and object properties this time, and it returns the suspect possessed by BOB like we wanted. But there's probably a way to clear that up a little. With .find our function again can shrink to a single line:
let suspects = [{name: 'Philip Gerard', possessedBy: 'MIKE'}, {name: 'Leo Johnson'}, {name: 'Benjamin Horne', possessedBy: 'greed'}, {name: 'Log', possessedBy: 'Log Lady’s late Husband'}, {name: 'Jacques Renault'}, {name: 'Leland Palmer', possessedBy: 'BOB'}];
suspects.find(suspect => suspect.possessedBy === 'BOB').name;
// returns 'Leland Palmer'
WOW BOB WOW! .find was able to distill that four-line function into one clear statement. And we didn't even have to use the Tibetan method.
Filter
The final standard iterator to explore is the filter method. Filter takes an array and returns another array of smaller or equal size depending on the condition we pass into the filter. In 2012, I would have (well, I actually did) filtered a hand of playing cards with a for-loop:
// Go Fish!
// myHand and oppHand are defined arrays of cards within the relevant scope, each with assigned values (Ace, King, 8) for this.valueID. myScore & oppScore are the scores for the two players.
function scoreCheck(player,valueToCheck) {
var i;
var y;
var handCount = 0;
for (i = 0; i < player.length; i++) {
if (player[i].valueID === valueToCheck) {
handCount += 1;
}
if (handCount >=4 && player === oppHand) {
console.log("Your opponent collected a set of " + cardValue + "s.");
oppScore += 1;
for (y = 0; y < oppHand.length; y++) {
if (oppHand[y].valueID === valueToCheck) {
discard[discard.length] = oppHand[y];
oppHand.splice(y,1);
y--;
}
}
}
} else if (handCount >= 4 && player === myHand) {
console.log("You collected a set of " + cardValue[valueToCheck] + "s.");
myScore += 1;
for (y = 0; y < myHand.length; y++) {
if (myHand[y].valueID === valueToCheck) {
discard[discard.length] = myHand[y];
myHand.splice(y,1);
y--;
}
}
}
}
}
AHHHHHHHHHH!
This is what pre-2011 JavaScript required. In early 2012, when I wrote that, the .filter method had been around for less than a year and was not included in the cirriculum I was following. Today, my scoreCheck() method would be a little easier to follow.
let myHand = //an array of cards
let oppHand = //an array of cards
function scoreCheck(playerHand, valueToCheck) {
cardsOfValue = playerHand.filter( card => card.valueID === valueToCheck )
if (cardsOfValue.length === 4 && playerHand === myHand ) {
console.log(`You collected a set of ${valueToCheck}s!`);
myHand = playerHand.filter (card => card.valueID !== valueToCheck);
}
else if (cardsOfValue.length === 4 && playerHand === oppHand) {
console.log(`Your opponent collected a set of ${valueToCheck}s`)
oppHand = playerHand.filter (card => card.valueID !== valueToCheck);
}
}
My scoreCheck function for 'Go Fish!' has been shrunk from a twisting series of for-loops that would even at times need to subtract from the iterator index value because it was removing cards during the process to a few simple if statements and calls to the .filter method.
Conclusion
The for-loop is mostly dead, having been replaced by methods that are either more dynamic like the ones demonstrated here or by more advanced loops (for..in has the ability to pull index values). It's only a few strange cases where a traditional for-loop still makes sense to use on an array, such as when we want to start iterating over the array at an index value after 0, but there are usually even work-arounds in those situations. For very large datasets and depending on the browser, a for-loop might actually iterate more quickly than its most direct sibling, ForEach, but the results there are mixed.
Additionally, for-loops are pretty awful at handling edge cases such as empty, undefined, or null values in arrays, while Javascript's built-in iterators usually account for those cases. The use of iterators results in shorter, sharper, and understandable code.