Arrays and Objects

The object of the superior programmer is truthy.
— Confucius

Arrays

Let's say you're writing a task application and you need to store a bunch of tasks. You could declare a separate variable for every task like this:

const task1 = 'First task';
const task2 = 'Second task';
const task3 = 'Third task';

However, this would quickly become very tedious. Additionally, you probably want to be able to add or delete tasks in your application. Adding and deleting variables will become even more tedious. It would become almost as tedious as repeating the word tedious over and over. Did we mention that this is really tedious?

As you can see, we need a way to store multiple values in a single variable. We can do this with arrays.

A JavaScript array is an ordered collection of multiple values. You can declare an array using an array literal (also called an array initializer in this context):

const tasks = ['First task', 'Second task', 'Third task'];

Note that an array is no longer a primitive type. Instead, arrays have the type object:

console.log(typeof tasks); // object

You can access individual elements of an array using the index notation. This works by writing the name of the array, followed by the position of the element you want to retrieve inside square brackets []. Note that when we count the indices (positions), we start at 0, not at 1:

console.log(tasks[0]); // First task
console.log(tasks[1]); // Second task

If the array index is too big, trying to access the element at that index will return undefined:

console.log(tasks[3]); // undefined

You can get the length of an array using .length:

console.log(tasks.length); // 3

JavaScript has elegant syntax for working with arrays. For example, if you want to assign variables based on values of an array, you would normally have to do something like this:

const firstTask = tasks[0];
const secondTask = tasks[1];
const thirdTask = tasks[2];

This is (you guessed it) tedious.

Instead, you can use the array destructuring assignment:

const [firstTask, secondTask, thirdTask] = tasks;
console.log(secondTask); // Second task

If you only care about some of the elements, you can use the spread (...) syntax:

const [firstTask, ...otherTasks] = tasks;

Something that commonly trips up beginners is trying to copy an array. Let's say you have an array of numbers called arr and you want to create a copy called arr2. You would probably try something like this:

const arr = [1, 2, 3, 4];
const arr2 = arr;

This is wrong. We can see that this is wrong if we try to change the first element of arr and then have a look at arr[0] and arr2[0]:

arr[0] = 5;
console.log(arr[0]); // 5
console.log(arr2[0]); // 5

Uh-oh! That's probably not what we want. The reason for this behaviour is that arr and arr2 both point to the same array. Remember how we were careful to introduce a variable as a storage location together with a symbolic name? Well, it turns out that different symbolic names may refer to the exact same storage location.

You can visualize it like this:

Here we have a storage location containing the values 1, 2, 3 and 4 somewhere. We also have two symbolic names arr and arr2. While the symbolic names are different, they point to the same storage location. Therefore, if we change the storage location, we will observe a change via both symbolic names.

In order to actually copy the values, we can use the spread syntax again:

const copied = [...arr];

Let's check that this is indeed an actual copy:

arr[0] = 5;
console.log(arr[0]); // 5
console.log(copied[0]); // 1

This looks good. Here is the mental picture you should have in your head for copying an array:

If you only briefly skimmed the section on array destructuring and the spread syntax, go back again and read it carefully. These two concepts will come up a lot in the following chapters (much more often than you think right now).

Objects

Let's return to our imaginary (as of now) task application. A task will probably be something more than just a string. For example, it might contain an ID, a title and a description. We could, again, try to store these values in separate constants:

const taskId = 1;
const taskTitle = 'Read the Next.js book';
const taskDescription = 'Read and understand the Next.js book.';

As you can probably guess, this will quickly become tedious (oh no, not this again).

Objects to the rescue! These allow us to store name-value pairs inside a single variable. Here is how we might create a task object that contains all the information we want to know about a task:

const task = {
  id: 1,
  title: 'Read the Next.js book',
  description: 'Read and understand the Next.js book.',
};

Every such name-value pair is called a property. We can access properties using the dot notation . or the square bracket notation []. For example, we can access the title property of the task object by writing task.title or task['title']. Try it out:

console.log(task.id); // 1
console.log(task.title); // Read the Next.js book
console.log(task.description); // Read and understand the Next.js book.
console.log(task['id']); // 1
console.log(task['title']); // Read the Next.js book
console.log(task['description']); // Read and understand the Next.js book.

Note that we will practically always use the dot notation.

Remember how you accessed the length of an array using arr.length? You can do that because every array has a property called length that indicates the length of that array.

Properties don't have to be primitive values. They can also be other objects.

Generally speaking, you can arbitrarily nest objects and arrays. For example, here is how you can nest an object inside an object:

const user = {
  name: 'John Doe',
  task: {
    id: 1,
    title: 'Read the Next.js book',
    description: 'Read and understand the Next.js book.',
  },
};

You can access the title property of the user.task object like this:

console.log(user.task.title); // Read the Next.js book

If you try to access a property that doesn't exist, the result will be undefined:

console.log(task.date); // undefined

Sometimes you want to explicitly indicate that a property may be absent. For example, a person may not have a task assigned to them. You can write something like this:

const person = {
  name: 'John Doe',
  task: undefined,
};

Instead of undefined you can also null which represents the absence of an object value. Note that there is no separate null data type. Instead, null is just a special object:

console.log(typeof null); // object

Here is how you can use null to represent the absence of a property:

const person = {
  name: 'John Doe',
  task: null,
};

Whether to use undefined or null in this situation is largely convention. Throughout this book we will always use undefined. Nevertheless, we want to emphasize that it's totally fine to use null instead of undefined in this situation. Just pick a style and be consistent.

You can use the destructuring assignment when working with objects. This is similar to arrays:

const task = {
  id: 1,
  title: 'Read the Next.js book',
  description: 'Read and understand the Next.js book.',
};
const { id, title, description } = task;

And just as with arrays, you can use the spread syntax with objects:

const taskWithAssignee = {
  assignee: 'John Doe',
  ...task,
};
console.log(taskWithAssignee);

This will output:

{
  assignee: 'John Doe',
  id: 1,
  title: 'Read the Next.js book',
  description: 'Read and understand the Next.js book.'
}

Note that objects are more than just containers for values. We will return to this later.

Using const with Arrays and Objects

There is often some confusion regarding the use of const with arrays and objects.

For example, it seems strange that you can change the elements of a const array:

const arr = [1];
arr[0] = 2; // Totally valid
arr.push(3); // Totally valid

It seems equally strange that you can change the properties of a const object:

const obj = { prop: 1 };
obj.prop = 2; // Totally valid

Such assignments are possible because const only applies to the constant itself, not to the contents of the constant. This means that the only thing you can't do is to change what the constant is pointing to altogether.

For instance, this is not possible:

const arr = [1];
arr = [2, 3]; // Not valid

Similarly, this is also not possible:

const obj = { prop: 1 };
obj = { prop: 2 }; // Not valid

This means that const is a pretty weak guarantee when working with arrays and objects. After all, you often change elements of arrays and objects, but rarely change what the array and/or object is pointing to in its entirety.

Nevertheless, you should use const even when working with arrays and objects. A weak guarantee is better than no guarantee at all.