Carried Away With Arrays
March 15, 2019
Arrays are one of my favorite data structures in JavaScript (and Python). They can store all sorts of goodies: numbers, strings, objects, more arrays, and so forth. JavaScript has a wealth of methods for dealing with the Array object. In this post, we'll examine a few of them to demonstrate the power and usefullness of arrays.
A sample data array storing items in an online shopping cart will be used throughout the post:
const cart = [
{ department: 'clothing', description: 'shirt', price: 17 },
{ department: 'clothing', description: 'hat', price: 8 },
{ department: 'bedroom', description: 'pillow', price: 12 },
{ department: 'kitchen', description: 'bowls', price: 44 },
{ department: 'kitchen', description: 'plates', price: 30 },
{ department: 'toys', description: 'frisbee', price: 7 }
]
Map/Filter/Reduce
Map, filter, and reduce...the holy trio of higher-order array methods.
Map
map is perhaps my most commonly used array method as it creates a new array based on a function applied to each item in the current array, going from left to right.
Example 1: It's our lucky day and upon checking out, we're notified that we get 10% off each item. How do we update our cart to reflect the new prices?
const cartWithDiscount = cart.map(item =>
({...item, price: item.price * .90 }));
// [{department: "clothing", description: "shirt", price: 15.3},
// {department: "clothing", description: "hat", price: 7.2},
// {department: "bedroom", description: "pillow", price: 10.8},
// {department: "kitchen", description: "bowls", price: 39.6},
// {department: "kitchen", description: "plates", price: 27},
// {department: "toys", description: "frisbee", price: 6.3}]
A new array is created by mapping over each item in cart
and applying a function that returns a new object where the value of the price
property on each item is reduced by 10%. Arrow functions are used here, which include an implicit return
statement. The spread operator is also used to copy each object and then update values of the price
property during the mapping process.
Example 2: We have a coupon for $5 off items from the "kitchen" department. How do we shave off five bucks from the bowls and plates in our cart?
const cartWithCoupon = cart.map(item =>
item.department === 'kitchen'
? { ...item, price: item.price - 5 }
: item
);
// [{department: "clothing", description: "shirt", price: 17}.
// {department: "clothing", description: "hat", price: 8},
// {department: "bedroom", description: "pillow", price: 12},
// {department: "kitchen", description: "bowls", price: 39},
// {department: "kitchen", description: "plates", price: 25},
// {department: "toys", description: "frisbee", price: 7}]
Similar to Example 1, only this time a ternary operator is added to check whether the department
is "kitchen" or not. If so, a new object with the item's price reduced by $5 is returned, and if not, the original item unchanged is returned.
Filter
Alongside map
, the filter function exists to create a new array based on whether each item in the current array passes some defined condition. Whereas map
will not change the number of items in the new array, filter
can decrease the number of items in the new array.
Example 3: Turns out that we really only need some new clothes; a new pillow, bowls, plates, and frisbee will have to be purchased another time. How do we filter our cart so that only items in the "clothing" department remain?
const onlyClothing = cart.filter(item => item.department === 'clothing');
// [{department: "clothing", description: "shirt", price: 17},
// {department: "clothing", description: "hat", price: 8}]
Again, arrow functions and implicit return statements are used here for brevity. But let's see the code without arrow functions to help reveal what exactly is going on.
const onlyClothing = cart.filter(function(item) {
if (item.department === 'clothing') {
return item;
}
});
We more clearly see that a callback function is applied on each item
such that only items in the clothing department are returned in the new array. In this case, the new array has a length of 2 since we have 2 clothing items in the original cart.
Example 4: We only have $10 to spend on this shopping trip, so how can we identify which items are $10 or less in our cart?
const tenDollarsOrLess = cart.filter(item => item.price <= 10);
// [{department: "clothing", description: "hat", price: 8},
// {department: "toys", description: "frisbee", price: 7}]
As seen in Example 3, we test each item on a condition - in this case, whether the item's price is $10 or less - and if the item passes that condition, we return it in a new array. Since we have two items that cost $10 or less (hat and frisbee), tenDollarsOrLess
has a length of two.
Reduce
As the name suggests, reduce is used to shrink an array of items down to a single value. Specifically, a defined "reducer" callback function is executed on each array item, which takes at least two arguments:
- Accumulator (often abbreviated as "acc") that gathers or accumulates return values from the callback
- Current value (often abbreviated as "curr") that represents the current item being processed in the array
Optionally, the callback can also take arguments pertaining to the current index being processed and the source or original array that reduce
was called upon.
The initial value of the accumulator in our callback also needs to be defined. This can be a number (if we want to sum values in the array), an array itself (if we want to distill array items into another array), an object (if we want to copy array items into a new object), and so forth.
Example 5: What is the total cost of items in our cart?
const totalCost = cart.reduce((acc, curr) => acc + curr.price, 0);
// 118
Let's break this down. The accumulator is a number starting at 0. We loop over each item in the cart and add the current item's price to this accumulator, ultimately revealing the total cost of items in the cart.
Example 6: We are interested in counting the number of items in our cart per department. How do we accomplish this using reduce
?
const countInstances = cart.reduce((acc, curr) => {
if (acc.hasOwnProperty(curr.department)) {
acc[curr.department]++;
} else {
acc[curr.department] = 1;
}
return acc;
}, {})
// {clothing: 2, bedroom: 1, kitchen: 2, toys: 1}
The reducer callback has the usual acc
and curr
arguments, though this time the accumulator is initialized as an empty object {}
. The body of this arrow function is now multiple lines, requiring it to be wrapped in curly braces and include an explicit return
statement. The logic in the function checks whether acc
(an empty object to start) has a property of the current item's department value. If that property exists (i.e. we already have an item from that department), then the value of that property is incremented. If no property exists, a new property on acc
with the current item's department is created and assigned a value of 1.
Example 7: Let's say another piece of our code needs to have the cart transformed to a nested array where the inner array contains the item's description and price, e.g. [ ['shirt', 17], ['hat', 8] ]
. How do we accomplish this using reduce
?
const nestedCartArray = cart.reduce((acc, curr) =>
acc.concat([ [curr.description, curr.price] ]), []);
// [ ["shirt", 17],
// ["hat", 8],
// ["pillow", 12],
// ["bowls", 44],
// ["plates", 30],
// ["frisbee", 7] ]
This time our accumulator is initialized as an empty array and inner arrays are concatenated to it via the return value in the callback reducer function.
Alternatively, map
could be used for this transformation:
const nestedCartArrayWithMap = cart.map(item =>
[item.description, item.price]);
Though this example demonstrates how powerful reduce
is by containing map
capabilities and so much more.
Sorting
JavaScript conveniently includes a built-in sort method, though the algorithm is not standard across browsers. There's also an interesting history of sorting inside Google's V8 JS engine.
Example 8: How do we sort items in our cart from lowest to highest price?
const sortedByPrice = cart.sort((a, b) => a.price - b.price);
// [{department: "toys", description: "frisbee", price: 7}
// {department: "clothing", description: "hat", price: 8}
// {department: "bedroom", description: "pillow", price: 12}
// {department: "clothing", description: "shirt", price: 17}
// {department: "kitchen", description: "plates", price: 30}
// {department: "kitchen", description: "bowls", price: 44}]
//
This seems a bit like magic using arrow functions and seemingly random a
and b
arguments. So let's write the code in its long-form to more clearly understand the logic.
const sortedByPrice = cart.sort(function(curr, next) {
if (curr.price < next.price) return -1;
else if (curr.price > next.price) return 1;
return 0;
})
Now we see that the callback is a comparison function that takes in the current and next items in the array, or a
and b
, as is commonly used. If the price of the current item is less than the next item's price, -1 is returned, which in-place sorts curr
to a lower index than next
. On the other hand, if the current item's price is greater than the next item's price, +1 is returned, and curr
is in-place sorted to a higher index than next
. If the prices between the two items are equal, 0 is returned and the indices are unchanged.
Want to instead sort prices from high-to-low? Just reverse the return
statements in the long-form or use b.price - a.price
in the arrow function. The beauty of sort
is how flexible and customizable the comparison function can be.
Locating items and indices
Oftentimes we want to determine whether an item exists in an array and if so, what its index is. There are several handy methods to accomplish these tasks.
indexOf
indexOf returns either the first index where a given array element is found or -1 if the given element is not found.
Example 9: Do we have any items in our cart from the "toys" department?
const anyToysInCart = cart.map(item => item.department)
.indexOf('toys') !== -1 ? 'yes' : 'no';
// 'yes'
Two methods are chained together - first mapping over cart
to extract the departments of each item and then checking whether "toys" exists in that newly-created array of department names. Since our frisbee is classified in the "toys" department, indexOf(toys)
will yield its index in the array (in this case, 5). Since 5 doesn't equal -1, our ternary operator will return yes
as the value to anyToysInCart
.
includes
Minus Internet Explorer, all other major browsers support the includes method. While indexOf
returns a number (either the index of a given element or -1 if missing), includes
returns a Boolean true
or false
in its search for an element in the array.
Example 10: Do we have any items in our cart from the "kitchen" department? What about the "garden" department?
const anyKitchenItems = cart.map(item => item.department)
.includes('kitchen'); // true;
const anyGardenItems = cart.map(item => item.department)
.includes('garden'); // false
Similar to Example 9, we first create a new array of department names using map
and check whether "kitchen" or "garage" exist in that array.
find
A more flexible method for element searching in arrays is accomplished through find. find
returns the value of the first element that passes a defined testing callback function.
Example 11: We need to pare down our shopping cart and want to locate the first pricey item in it. What is the first item in our cart with a price over $25?
const firstPriceyItem = cart.find(item => item.price > 25);
// {department: "kitchen", description: "bowls", price: 44}
Bowls are the first item in cart
with a price greater than $25, hence it is returned from the find
method.
'some' and 'every' questions
Often we'll ask questions about data in arrays like "Does at least one item pass some condition?" and "Do all items pass some condition?". The some and every methods return Boolean true
or false
expressions as answers to those questions, respectively.
Example 12: Do any items in our cart have a price above $40? What about $50?
const anyAbove40 = cart.some(item => item.price > 40); // true
const anyAbove50 = cart.some(item => item.price > 50); // false
The some
method uses a callback comparison function to test the truthiness of each item in the array. If any item is truthy, then true
is returned. If not, then false
is returned.
every
has an identical structure to some
, only this time all items must be deemed truthy for true
to be returned.
Example 13: Do all items in our cart have a price less than $100? Less than $40?
const allBelow100 = cart.every(item => item.price < 100); //true
const allbelow40 = cart.every(item => item.price < 40); //false
This post just scratched the surface of the amazing world of arrays in JavaScript. Curious to learn more about array methods? Simply type an array into the console and click on the __proto__
tag to reveal dozens of delicious methods inherited from Array.prototype
. Or read about them on MDN.