Unit 2 Objects & Arrays
Lesson 5: New Array Methods
Arrays are probably the most common data structure used in JavaScript. We use them to hold all kinds of data but sometimes getting the data we want into or out of the array isn’t as easy as it should be. However those tasks just got a whole lot easier with some of the new array methods that we will cover. In this lesson we will cover the following:
- Constructing Arrays with
Array.from
- Constructing Arrays with
Array.of
- Constructing Arrays with
Array.prototype.fill
- Constructing Arrays with
Array.prototype.includes
- Constructing Arrays with
Array.prototype.find
Priming Exercise: Consider this snippet of jQuery code that grabs all the DOM nodes with a specific CSS class and sets them to be red. If you were going to implement this from scratch what considerations would you have to make? For example, if you were to use document.querySelectorAll which returns a NodeList (not an Array) how would you iterate each node to update its color?
$('.danger').css('color', 'red')
5.1 Constructing Arrays with Array.from
Before ES6, the common way of converting an array-like object into an array was by using the slice
method on Array.prototype
and applying it to the array-like object. This works because calling slice on an array without any arguments simply creates a shallow copy of the array. By applying the same logic to an array-like object, you still get a shallow copy but the copy is an actual array.
Array.prototype.slice.call(arrayLikeObject)
// or a shorter version:
[].slice.call(arrayLikeObject)
With that in mind we can fix our avg
by converting the arguments object into an array using this trick.
function avg() {
// convert arrayLikeObject to array
const args = [].slice.apply(arguments)
const sum = args.reduce(function(a, b) {
return a + b
})
return sum / args.length
}
This shortcut is no longer needed with Array.from
.. The purpose of Array.from
is to take an array-like object and get an actual array from it. The array like object is an object with a length
property.
- The length property is used to determine the length of the new array, as well any integer properties that are less than the length property will be added to the newly created array at the appropriate index.
- For example, a
string
has a length property and numeric properties indicating the index of each character. So callingArray.from
with a string will return an array of characters. - The
arguments
object can be converted into an array using this technique as well.
// Listing 5.3 Updating the `avg` function to use `Array.from`
function avg() {
const args = Array.from(arguments)
const sum = args.reduce(function(a, b) {
return a + b
})
return sum / args.length
}
avg(1, 2, 3)
avg(100, 104)
avg(10, 99, 5, 46)
Another common use case for needing Array.from
is in conjunction with document.querySelectorAll
. document.querySelectorAll
returns a list of matching DOM nodes, but the object type is a NodeList
, not an array.
5.2 Constructing Arrays with Array.of
let a = new Array(1, 2, 3) // The `a` array contains the 3 values: 1, 2, 3.
let b = new Array(1, 2) // The `b` array contains the 2 values: 1 and 2.
let c = new Array(1) // Finally the `c` array contains the single value: undefined.
It is kinda of a quirk than when you declare new Array(1)
you get an array with an undefined value in it.11 The reason for this is a special behavior in the Array constructor that if there is only one argument and that argument is an integer, it creates a sparse array of length n
, where n
is the number passed in as an argument.
- To avoid this quirk you can use the
Array.of
factory function that works more predictably.
let a = Array.of(1, 2, 3)
let b = Array.of(1, 2)
let c = Array.of(1)
The arrays created with Array.of are the same accept for array c
, which more intuitively this time contains the single value 1
. At this point, you may be thinking why not just use an array literal? such as:
let a = [1, 2, 3]
let b = [1, 2]
let c = [1]
In most situations an array literal is actually the preferred way to create arrays. However there are some situations where an array literal will not work. One such situation is using subclasses of arrays.footnote
.
- Imagine you are using a library that provides a subclass of array called
AwesomeArray
. Because it is a subclass ofArray
it has the same quirks. So you cannotsimplycallnewAwesomeArray(1)
because you will get anAwesomeArray
with a single undefined value. However you cannot use an array literal here because that will give you an instance ofArray
notAwesomeArray
. - You can however use
AwesomeArray.of(1)
and get instance ofAwesomeArray
with a single value of1
.
Spot Quiz 2: Which of the following returns an array with undefined values?
new Array() // return [] ?
new Array(true)
new Array(false)
new Array(5) // return undefined [ , , , , ]
new Array("five")
So now we can construct an array with a single numeric value with Array.of(50)
but what if we did indeed want an array with 50 values. We could go back to new Array(50)
but that still has issues as we will see in the next lesson.
5.3 Constructing Arrays with Array.prototype.fill
Image you are creating a tic tac toe game.
const board = new Array(9).map(function(i) {
return ' '
})
The thought process here is that we initialize the array with 9 values, all of them undefined
. Then we use map to convert each undefined value into a space. But this will not work. When you create an array like new Array(9)
it does not actually add nine undefined
values to the new array. It merely sets the newly created array’s length property to 9.
If you are confused by that, let’s first discuss a bit about how arrays work in JavaScript. An array is not as special as many people think it is. Other than having a literal syntax like [ ... ]
it is no different than any other object. When you create an array like ['a', 'b', 'c']
internally that array looks like:
{
length: 3,
0: 'a',
1: 'b',
2: 'c'
}
Of course, it will also inherit several methods such as push
, pop
, map
, etc. from Array.prototype
. When performing iterative operations like map
, forEach
, etc. the array will internally look at it’s length then check itself for any properties within the range starting at zero and ending at length.
When you create an array via new Array(9)
many developers believe that internally the array will look like:
{
length: 9,
0: undefined,
...
}
But in fact it will look like:
{
length: 9
}
It will have a length of 9 but it will not have the actual 9 values. These missing values are called holes.
- Methods like
map
do not work on holes so that is why our attempt at creating an array with 9 spaces did not work. - There is a new method called
fill
that fills an array with a specified value.- When filling the array it does not care if at a given index there is a value or a hole so it will work perfectly for our use:
const board = new Array(9).fill('')
So now we have covered several ways to construct arrays containing the values we want. Now lets look at some new methods of searching for those values once the arrays are constructed.
5.4 Searching in Arrays with Array.prototype.includes
We learned in lesson 3 that strings have a new method on their prototype called includes for determining if a string contains a value. Arrays have also been given this method and it works similarly as seen in listing 5.9
// Listing 5.4 Using `includes` to check if an array contains a value
const GRID = 'grid'
const LIST = 'list'
const availableOptions = [GRID, LIST]
let optionA = 'list'
let optionB = 'table'
availableOptions.includes(optionA) // return true
availableOptions.includes(optionB) // return false
With the **String.prototype.includes**
method you are checking if a string contains a substring. Array.prototype.includes
works similarly but you are checking if any of the values at any of the array’s indices are the value you that are checking against.
Previously indexOf
was used for determining if a value were in an array. For example:
This works fine but often led to bugs if the developer forgot that they need to compare the result to -1, not truthiness because if the given value were at the 0 index, they would get back 0, a falsy value and vice versa if the value were not found they would get back -1, a truthy value. But this gotcha can be avoided now by just using includes which returns a boolean instead of an index.
5.5 Searching in Arrays with Array.prototype.find
Imagine you have an array of records cached from a database. When a request is made for a record you want to check the cache first to see if it has the record and return it before hitting the database again. You may end up writing code that resembles listing 5.4:
// Listing 5.5 The `filter` method returns all matches even if we only want one.
function findFromCache(id) {
let matches = cache.filter(function(record)
return record.id === id
})
if (matches.length) {
return matches[0]
}
}
The findFromCache
function from listing 5.4 would certainly work but what happens when the cache has 10,1000 records and it finds the one it’s looking for at only the 100th try? Well in it’s current implementation it would still check the remaining 9,900 records before returning the one it found. This is because the purpose of the filter
function is for returning all the records that match. We are only expecting one record to match and once found we just need the record back. That is exactly what Array.prototype.find
does, it searches through an array much like Array.prototype.filter
but as soon as it finds a match, it immediately returns that match and stops searching this array.
We can rewrite our findFromCache function to make use of the find function like so:
function findFromCache (id) {
return cache.find(function(record) {
return record.id === id
}
}
Another nice thing about find, is because it returns the item that matched instead of an array of matches, we don’t have to pull it out of the array afterwards, in fact we can simply just return the result of find directly.
5.6 Summary
Array.from
Array.of
Array.prototype.includes
Array.prototype.find
Array.prototype.fill