[book note] Manning MEAP: JavaScript Next – Unit 2: Objects & Arrays

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 calling Array.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 of Array it has the same quirks. So you cannot simplycallnewAwesomeArray(1) because you will get an AwesomeArray with a single undefined value. However you cannot use an array literal here because that will give you an instance of Array not AwesomeArray.
  • You can however use AwesomeArray.of(1) and get instance of AwesomeArray with a single value of 1.

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

Lesson 6: Object.assign

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.