In the previous section, we learned about arrays, and we saw how we can use for-loops to iterate over them. As is often the case, however, we've started with "the hard way" of doing things, and now we're going to learn an easier way to work with Arrays.
It turns out that JavaScript arrays have a much richer set of built-in methods
that make iterating over them and calculating a properties a real
pleasure. Most modern JavaScript developers prefer these to the traditional
for
-loops, and -- for many applications -- you can actually give a compelling
argument for why this approach is superior.
In the previous section, we saw how to sum a list of numbers using a for-loop.
var sum = function (listOfNumbers) {
var index;
var sum = 0;
for (index = 0; index < listOfNumbers.length; index = index + 1) {
sum = sum + listOfNumbers[index];
}
return sum;
}
The above approach to iterating over arrays is pretty common, but there's a
better way. In cases like this, it's much better to use the array's forEach
method. The forEach
method actually takes a function as an argument and then
applies the function to each element.
For example, if we want to print each element of an array called arr
, we can
use the forEach
function like this:
var arr = [5,6,7,8];
var printElement = function (elt) {
console.log(elt);
}
arr.forEach(printElement);
//=> 5
//=> 6
//=> 7
//=> 8
In this example, we first defined the function called printElement
and then
sent it as a parameter to the forEach
function. It turns out that you don't
actually have to define the function first -- you can use an anonymous
function to specify the action. This is a very common pattern in JavaScript, so
you should probably get used to it.
var arr = [5,6,7,8];
arr.forEach(function (elt) {
console.log(elt);
});
The forEach
loop takes the function that is the parameter, and then sends each
array element to it as an argument.
Previously, I mentioned that this approach is superior to using a for
-loop. Why
is that the case? The primary reason is that it makes it so you don't have to
track array indices, which means you need one less variable (index
in this
case). This actually eliminates the potential for an entire class of programming
errors (known as off-by-one errors) from your code.
More generally speaking, the fewer variables that you have to define and keep track of, the fewer potential bugs your program will have.
We can use this approach to rewrite the sum
function using fewer variables,
and leaving fewer places that we could have an error.
var sum = function (listOfNumbers) {
var sum = 0;
listOfNumbers.forEach(function (number) {
sum = sum + number;
});
return sum;
}
It turns out that JavaScript arrays have even more functions that allow you to easily manipulate the data that they contain. For example, suppose we wanted to take a given array of numbers, and return an array of numbers that are the the previous numbers doubled.
Specifically, it would behave like this:
var numbers = [1,2,3,4,5,6];
var doubles = doubleNumbers(numbers);
console.log(doubles);
//=> [2,4,6,8,10,12]
Given what we learned previously, here is a reasonable approach to solving this problem.
var doubles = function (nums) {
var index;
var result = []; // create an empty array
for (index = 0; index < nums.length; index = index + 1) {
result.push(nums[index] * 2);
}
return result;
}
We saw a few examples like this in the practice portion of the previous
section. We would accept an array as an argument, and return a new array as the
result. Sometimes, as in the case of the mapToTags
function, the result array
was simply the original array with some straight-forward operation applied to
it.
One thing that we can do to simplify this pattern is to remove the index
variable by using a forEach
loop.
var doubles = function (nums) {
var result = []; // create an empty array
nums.forEach(function (num) {
result.push(num * 2);
});
return result;
}
But there's an even better way! The pattern of constructing a new array by
applying a function to every element occurs so frequently, we have a function
called map
that does exactly that. In fact, it allows us to remove the
result
variable altogether!
var doubles = function (nums) {
return nums.map(function (num) {
return num * 2;
});
}
That's all map
does -- it returns a new array that is the old array with the
specified function applied to each element!
Here's a few other examples that we can type right into the console. Let's
suppose we want to create an array that is simply the first letters of an array
of strings. This would be a perfect candidate for the map
function.
["hi", "everyone", "loves", "lists", "of", "words"].map(function (word) {
return word[0];
});
//=> ["h", "e", "l", "l", "o", "w"]
Similarly, we can map a list of boolean values to their opposites.
[true, false, true, true, false, true].map(function (val) {
return !val;
});
//=> [false, true, false, false, true, false]
And it turns out that we don't have to use an anonymous function. For example, in the last practice problem from the last section, we got the tags of a set of HTML elements. That function can be simplified to something like this (minus the error checking, of course).
var mapToTags = function (htmlElements) {
return htmlElements.map(getTagName);
}
Since map
returns an array, we can immediately chain a call to
forEach
. This means we effectively call the next function on the returned
array.
var numbers = [1,2,3,4];
numbers.map(function (number) {
return number * 2;
}).forEach(function (number) {
console.log(number);
});
//=> 2
//=> 4
//=> 6
//=> 8
In the previous problem section, we created a range
function that generated an
array with numbers from the given range. Since that function returns an array,
we can chain that as well.
range(1,4).map(function (number) {
return number * 2;
}).forEach(function (number) {
console.log(number);
});
//=> 2
//=> 4
//=> 6
//=> 8
Chaining functions with the range
function gives us a nice approach to
learning about the other features of JavaScript arrays.
There's another method that is similarly convenient:filter
. That allows us to
create a new array that only consists of the elements of the previous array that
pass some basic boolean test. For example, suppose we wanted all of the even
numbers in an array.
var nums = [ 5, 10, 15, 20, 25, 30, 35, 36, 37, 38, 39, 40 ];
nums.filter(function (elt) {
return elt % 2 === 0;
});
//=> [ 10, 20, 30, 36, 38, 40 ]
Combining this with the range
function, we can print out all the even numbers
less than 100 in a pretty interesting way:
range(0,100).filter(function (elt) {
return elt % 2 === 0;
}).forEach(function (elt) {
console.lot(elt);
});
In the previous section, we had a practice problem where we wrote a function
called filterToLol
that accepts an array of strings and returns an array that
consists of all of the original strings that contain "lol" in any case. We can
recreate that function in a much simpler way by using the filter
method (minus
handling error conditions).
var filterToLol = function (arr) {
return arr.filter(function (tweet) {
return tweet.toLowerCase().indexOf("lol") > -1;
});
}
We've seen two very nice array methods that return arrays, but sometimes we'll want to compute a value based on array (instead of an array). For example, suppose we were trying to do some very basic analysis of the sentiment of a set of tweets, and wanted to know if any of the tweets in the list contain the word "awesome".
Using techniques found in the previous section and a forEach
loop, we can
create a function that does something like this.
var containsAwesome = function (tweets) {
var result = false;
tweets.forEach(function (tweet) {
if (tweet.toLowerCase().indexOf("awesome") > -1) {
result = true;
}
});
return result;
};
Or we could use the filter
method and check the length
property of the
resulting array.
var containsAwesome = function (tweets) {
return tweets.filter(function (tweet) {
return tweet.toLowerCase().indexOf("awesome") > -1;
}).length > 0;
};
This is a nice solution in that it removes the need for extraneous variables. But it has one significant disadvantage. Can you think of what it is?
If you think back, we learned how to break out of a for
-loop early when we
need to. There's no need to process the entire array if we've found what we ware
looking for. Unfortunately, the forEach
method (as well as the filter
and
map
methods) have no way to break out of a loop early.
It turns out that JavaScript has two functions that do have this property. The
some
method is a good example. It returns true
if any of the elements pass
the true
/false
test, and it immediately stops.
var containsAwesome = function (tweets) {
return tweets.some(function (tweet) {
return tweet.toLowerCase().indexOf("awesome") > -1;
});
};
How can we verify this behavior? Let's modify the function slightly.
var containsAwesome = function (tweets) {
return tweets.some(function (tweet) {
console.log("testing: " + tweet);
return tweet.toLowerCase().indexOf("awesome") > -1;
});
};
Now let's run it on a concrete example.
containsAwesome([ "sad tweet", "awesome tweet", "unprocessed tweet", "another tweet" ]);
//=> testing: sad tweet
//=> testing: awesome tweet
//=> true
You'll see that it only processes the first two tweets, because the second one contains awesome.
Similarly the every
function returns true
if all of the elements pass the
test, and it breaks out of the loop early if any of the elements evaluate to
false
.
var allAwesome = function (tweets) {
return tweets.every(function (tweet) {
return tweet.toLowerCase().indexOf("awesome") > -1;
});
};
But what if we have to compute something more complex than just a true
or
false
? For example, consider our favorite problem of summing a list of numbers
contained in an array. Is there some way we can leverage built-in array methods
so we can remove extra variables? It turns out we can -- the reduce
method
allows us to build up a very general computation by carrying an additional
function variable between calls.
Let's start by considering our solution to the sum
problem that uses the
forEach
method.
var sum = function (listOfNumbers) {
var sumSoFar = 0;
listOfNumbers.forEach(function (number) {
sumSoFar = sumSoFar + number;
});
return sumSoFar;
}
This allowed us to remove the index
variable we would need to maintain with a
traditional for
-loop, but we still have the sumSoFar
variable. How does
reduce
allow us to remove it?
Consider the following example.
var nums = [5,6,7,8,9,10];
nums.reduce(function (sumSoFar, number) {
return sumSoFar + number;
});
//=> 45
In this example, the anonymous function takes two values -- sumSoFar
and the
current number
. The job of the function is to combine these two values into
the next value for sumSoFar
.
But what's the value of sumSoFar
when the function is first called? By
default, will call the function the first time with sumSoFar
set to the first
value in the array, and number
set to the second. So this is the sequence of
calls to our function:
sumSoFar
is 5, andnumber
is 6. The result of the function is 11.sumSoFar
is 11, andnumber
is 7. The result of the function is 18.sumSoFar
is 18, andnumber
is 8. The result of the function is 26.sumSoFar
is 26, andnumber
is 9. The result of the function is 35.sumSoFar
is 35, andnumber
is 10. The result of the function is 45.
Since 10 was the last value in the array, reduce
returns 45. Generalizing
this, we can rewrite our sum
function with no local variables!
var sum = function (listOfNumbers) {
return listOfNumbers.reduce(function (sumSoFar, number) {
return sumSoFar + number;
});
};
Believe it or not, there are times when it doesn't actually make sense to make
the first argument to reduce
's function be the first element in the array. For
example, suppose we wanted to write a function that accepts an array of strings
and returns them combined into a paragraph as sentences.
paragraphify( [ "hello world", "this is a tweet, "goodbye" ] );
//=> Hello world.This is a tweet.Goodbye.
Assuming we have a function called capitalize
that capitalizes the first word
in a sentence, we can easily achieve this using a forEach
loop and a temporary
local variable.
var paragraphify = function (list) {
var result = ""; // initialize to empty list
list.forEach(function (sentence) {
result = result + capitalize(sentence) + "."; // add a space and a period
});
return result;
};
We might try to use reduce to remove the local result variable using reduce as follows.
var paragraphify = function (list) {
return list.reduce(function (paragraph, sentence) {
return result + capitalize(sentence) + ".";
});
};
Unfortunately, this won't capitalize or add a period after the first sentence.
paragraphify( [ "hello world", "this is a tweet, "goodbye" ] );
//=> hello worldThis is a tweet.Goodbye.
To make this approach work, we would need to give an explicit initial value to
paragraph, and then process all of the elements. We can do that by sending in a
second argument to reduce
, after the function. In this case, the second
argument will be the empty string.
var paragraphify = function (list) {
return list.reduce(function (paragraph, sentence) {
return result + capitalize(sentence) + ".";
}, ""); // <= the second argument is ""
};
This may look strange, but it's a common pattern in JavaScript.
It's actually worth noting that this isn't completely necessary in this case. We
could use the map
method and chaining a call to reduce
.
var paragraphify = function (list) {
return list.map(function (sentence) {
return capitalize(sentence) + ".";
}).reduce(function (paragraph, capitalizedSentence) {
return result + capitalizedSentence;
});
};
This has the same effect, and doesn't require the extra argument to reduce
.
The reduce
method is tricky enough that it warrants one more example. Let's
demonstrate how we can use it to find the smallest element in an array of
numbers.
var smallest = function (list) {
return list.reduce(function (smallest, current) {
var result = smallest;
if (current < smallest) {
result = current;
}
return result;
});
};
If we call this function on [ 5, 10, 3, 1, 9 ] the calls to the function look as follows.
smallest
is 5, andcurrent
is 10. The result of the function is 5.smallest
is 5, andcurrent
is 3. The result of the function is 3.smallest
is 3, andcurrent
is 1. The result of the function is 1.smallest
is 1, andcurrent
is 9. The result of the function is 1.
You'll see here that we've created a temporary local variable called result
to
store the smaller of the current element and the smallest element we've seen up
until that point. This maintains our pattern of always having a single return
statement in our functions (which is a good habit to get into).
This case is simple enough to where we could also do something like this to get
rid of the result
variable, but we do it at the expense of creating two
return
statements.
var smallest = function (list) {
return list.reduce(function (smallest, current) {
if (current < smallest) {
return current;
} else {
return smallest;
}
});
};
There is a trade-off here. Sometimes function with multiple return
statements
can be harder for people to understand, particularly if they are really
long. This one is small enough that it doesn't matter too much.
Another approach is to use the JavaScript ternary conditional operator, which is an operator that can be useful in these situations.
var smallest = function (list) {
return list.reduce(function (smallest, current) {
return (current < smallest) ? current : smallest;
});
};
The ternary operator takes in three arguments -- a boolean expression, a
resulting value if the expression is true
, and a resulting value if the
expression is false
.
I try to use these operators sparingly due to the fact that they are sometimes hard to read. But in this case I probably would use it.
We mentioned previously that arrays and strings are very similar, but that
strings don't enjoy some of the fun array methods. It turns out this isn't too
much of a problem, because it's very easy to convert between strings and
arrays. To change a string into an array that, we'll simply use the string's
split
method to turn it into an actual array of characters.
var greeting = "hello";
greeting.split("");
//=> ["h","e","l","l","o"]
The split
method actually gives us a little more flexibility. For example,
suppose we were dealing with comma-separated-values (CSVs).
var values = "gracie,loki,dahlia,ally"; //=> these are my dogs!
var names = values.split(",");
//=> [ "gracie", "loki", "dahlia", "ally" ]
We can even just split on spaces.
var tweet = "this is a tweet!";
var words = tweet.split(" ");
//=> [ "this", "is", "a", "tweet!" ];
Once we have the array, we can use all of our favorite methods!
names.map(capitalize);
//=> [ "Gracie", "Loki", "Dahlia", "Ally" ];
words.filter(function (word) {
return word.indexOf("!") > -1;
});
//=> [ "tweet!" ]
The inverse of split
is the array's join
function which will put a string
back together.
var array = ["h","e","l","l","o"];
array.join("");
//=> "hello"
Like the split
method, you can stitch an array of strings back together using
an arbitrary string.
names.map(capitalize).join(" -- ");
//=> "Gracie -- Loki -- Dahlia -- Ally"
words.join(";");
//=> "this;is;a;tweet!"
These methods make it very easy to do complex transformations on a string
without having to write a lot of complex for
-loops and maintain the value of a
lot of local variables.
For the first set of problems, open up the names.html
file in Chrome and then
open the developer console. There should be a variable defined called names
which you can confirm by typing it at the console.
names;
//=> Array[1559]
This is a list of all of the names of babies born in New York State since 2007. The list contains only unique names, but they aren't sorted in any specific order.
Using the map
, filter
, reduce
, some
, and every
methods, answer the
following questions.
-
How many baby names start with the letter 'z'?
-
How many baby names have the letter 'z' in them anywhere?
-
Create a new array that contains all of the names containing a 'w' with the first letter upper-case.
-
Do all of the names contain vowels?
-
Are there any names that have only two letters?
-
Is your name in the list?
-
Find the name that would come first alphabetically.
-
How many times does the letter 'z' appear in the list?
-
What is the maximum number of vowels in any name?
-
How many names have the maximum number of vowels that you found in the previous problem?