[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
    1
    Array.from
  • Constructing Arrays with
    1
    Array.of
  • Constructing Arrays with
    1
    Array.prototype.fill
  • Constructing Arrays with
    1
    Array.prototype.includes
  • Constructing Arrays with
    1
    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?


1
$('.danger').css('color', 'red')

5.1 Constructing Arrays with

1
Array.from

Before ES6, the common way of converting an array-like object into an array was by using the

1
slice

method on

1
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.


1
2
3
4
Array.prototype.slice.call(arrayLikeObject)

// or a shorter version:
[].slice.call(arrayLikeObject)

With that in mind we can fix our

1
avg

by converting the arguments object into an array using this trick.


1
2
3
4
5
6
7
8
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

1
Array.from

.. The purpose of

1
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

1
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
    1
    string

    has a length property and numeric properties indicating the index of each character. So calling

    1
    Array.from

    with a string will return an array of characters.

  • The
    1
    arguments

    object can be converted into an array using this technique as well.


1
2
3
4
5
6
7
8
9
10
11
12
// 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

1
Array.from

is in conjunction with

1
document.querySelectorAll

.

1
document.querySelectorAll

returns a list of matching DOM nodes, but the object type is a

1
NodeList

, not an array.


5.2 Constructing Arrays with

1
Array.of


1
2
3
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

1
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

1
n

, where

1
n

is the number passed in as an argument.

  • To avoid this quirk you can use the
    1
    Array.of

    factory function that works more predictably.


1
2
3
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

1
c

, which more intuitively this time contains the single value

1
1

. At this point, you may be thinking why not just use an array literal? such as:


1
2
3
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

1
arrays.footnote

.

  • Imagine you are using a library that provides a subclass of array called
    1
    AwesomeArray

    . Because it is a subclass of

    1
    Array

    it has the same quirks. So you cannot

    1
    simplycallnewAwesomeArray(1)

    because you will get an

    1
    AwesomeArray

    with a single undefined value. However you cannot use an array literal here because that will give you an instance of

    1
    Array

    not

    1
    AwesomeArray

    .

  • You can however use
    1
    AwesomeArray.of(1)

    and get instance of

    1
    AwesomeArray

    with a single value of

    1
    1

    .

Spot Quiz 2: Which of the following returns an array with undefined values?


1
2
3
4
5
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

1
Array.of(50)

but what if we did indeed want an array with 50 values. We could go back to

1
new Array(50)

but that still has issues as we will see in the next lesson.


5.3 Constructing Arrays with

1
Array.prototype.fill

Image you are creating a tic tac toe game.


1
2
3
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

1
undefined

. Then we use map to convert each undefined value into a space. But this will not work. When you create an array like

1
new Array(9)

it does not actually add nine

1
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

1
[ ... ]

it is no different than any other object. When you create an array like

1
['a', 'b', 'c']

internally that array looks like:


1
2
3
4
5
6
{
  length: 3,
  0: 'a',
  1: 'b',
  2: 'c'
}

Of course, it will also inherit several methods such as

1
push

,

1
pop

,

1
map

, etc. from

1
Array.prototype

. When performing iterative operations like

1
map

,

1
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

1
new Array(9)

many developers believe that internally the array will look like:


1
2
3
4
5
{
  length: 9,
  0: undefined,
  ...
}

But in fact it will look like:


1
2
3
{
  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
    1
    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
    1
    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:

1
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

1
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


1
2
3
4
5
6
7
8
9
10
// 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

1
**String.prototype.includes**

method you are checking if a string contains a substring.

1
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

1
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

1
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:


1
2
3
4
5
6
7
8
9
// 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

1
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

1
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

1
Array.prototype.find

does, it searches through an array much like

1
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:


1
2
3
4
5
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

  • 1
    Array.from
  • 1
    Array.of
  • 1
    Array.prototype.includes
  • 1
    Array.prototype.find
  • 1
    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.