Basic Data Structures

A set is like a wise emperor's court: it welcomes many, but never the same face twice.
— Ancient Chinese proverb

More on Strings

We already learned that strings represent sequences of characters. We also learned that they can be concatenated using the + operator and that you can get their length using the length property. However, this is not enough to efficiently work with strings.

Luckily, strings offer a wide range of additional functionality for pretty much every use case you will ever need—in this subsection we will briefly look at some of it.

First, you can access individual characters using the charAt function or array brackets:

const str = 'Hello';
console.log(str[1]); // e
console.log(str.charAt(1)); // e

Remember, there is no special character data type in JavaScript, i.e. typeof str[1] is simply 'string'.

Strings also offer a wide range of methods, most of which are self-explanatory:

const str = 'Hello';
console.log(str.concat(', World!')); // Hello, World!
console.log(str.includes('el')); // true
console.log(str.startsWith('He')); // true
console.log(str.endsWith('llo')); // true
console.log(str.indexOf('l')); // 2
console.log(str.lastIndexOf('l')); // 3
console.log(str.toLowerCase()); // hello
console.log(str.toUpperCase()); // HELLO

The substring method allows you to return a part of a string. You need to pass a start index and an end index:

console.log(str.substring(1, 3)); // el

Note that the start index will be included and the end index will be excluded when creating the substring.

There is also the substr method which is similar. However, substr is deprecated and, therefore, you shouldn't use it.

The trim method allows you to remove whitespace from the start and the end of a string. This is especially useful when you need to process user input and remove accidental whitespace at the start and the end of a string:

console.log(' Hello '.trim()); // Hello

The split method splits the string into substrings by a delimiter. For example, here is how you might split a comma-separated list into its items:

console.log('Task 1, Task 2, Task 3, Task 4'.split(','));

This will result in the following array:

[ 'Task 1', ' Task 2', ' Task 3', ' Task 4' ]

Note that the whitespace is not removed by the split method. You would need to iterate over the resulting array and use the trim method on each string to accomplish that.

More on Arrays

We already learned how to construct arrays and how to work with individual array elements. However, just like strings, arrays have a few additional methods that will often come in handy.

You can check whether an object is an array using the Array.isArray method:

console.log(Array.isArray([1, 2, 3])); // true
console.log(Array.isArray('123')); // false

Array.isArray is a static method. We talked about static methods in the section on classes.

You can create an array from an object with Array.from. To successfully use Array.from the object must be convertible to an array.

For example, a string is convertible to an array:

console.log(Array.from('123')); // [ '1', '2', '3' ]

If you try to use Array.from on an object that is not convertible to an array you will get an empty array:

console.log(Array.from(2)); // []

You can create an array from a variable number of arguments by using Array.of:

console.log(Array.of(1, 2, 3)); // [ 1, 2, 3 ]

Just like strings, arrays have a concat, includes, indexOf and lastIndexOf method:

console.log([1, 2, 3].concat([4, 5, 6])); // [ 1, 2, 3, 4, 5, 6 ]
console.log([1, 2, 3].includes(2)); // true
console.log(['a', 'b', 'b', 'c'].indexOf('b')); // 1
console.log(['a', 'b', 'b', 'c'].lastIndexOf('b')); // 2

The join method allows you to concatenate all elements in an array to a string, where the elements are separated by a delimiter:

console.log(['H', 'e', 'l', 'l', 'o'].join('')); // Hello
console.log(['H', 'e', 'l', 'l', 'o'].join(',')); // H,e,l,l,o

You can use the push method to add a new element to the end of an array:

const arr = [1, 2, 3];
arr.push(4);
console.log(arr); // [ 1, 2, 3, 4 ]

Of course, you could also do this with concat. However, when you just want to add a single element, it's more common to use push.

The pop element removes the last element of an array:

const arr = [1, 2, 3];
arr.pop();
console.log(arr); // [ 1, 2 ]

The reverse method allows you to reverse an array:

const arr = [1, 2, 3];
arr.reverse();
console.log(arr); // [ 3, 2, 1 ]

Arrays can be nested, resulting in multidimensional arrays. Accessing an element of such a nested array results in another array (of a lower dimension). Nested arrays can be flattened with the flat method:

const nestedArray = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];
console.log(nestedArray[1]); // [ 4, 5, 6 ]
console.log(nestedArray[1][2]); // 6
console.log(nestedArray.flat()); // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]

The for...of Loop

For...of loops allow you to iterate over arrays and strings (and a few other things) and perform a task for each element/character.

Let's say you want to print all tasks from a list named tasks. Previously, we would have used a regular for loop:

const tasks = ['Task 1', 'Task 2', 'Task 3'];
for (let i = 0; i < tasks.length; i++) {
  console.log(tasks[i]);
}

Instead you can use the for...of loop to accomplish the same thing:

const tasks = ['Task 1', 'Task 2', 'Task 3'];
for (let task of tasks) {
  console.log(task);
}

Both versions will output:

Task 1
Task 2
Task 3

As we already mentioned, you can use a for..of loop to iterate over a string as well:

const str = 'Task';
for (let char of str) {
  console.log(char);
}

This would output each character of the string, i.e.:

T
a
s
k

The general syntax of a for..of loop is

for (let variable of arrayOrString) {
  statements;
}

It should be noted that if you don't change the variable inside the loop, you can and should also declare it as const. Our first for...of example could therefore be rewritten to

const tasks = ['Task 1', 'Task 2', 'Task 3'];
for (const task of tasks) {
  console.log(task);
}

Just to reiterate, a for...of loop can iterate over more objects than just arrays and strings. However, this is out of scope for this book.

More on Objects

You can use the static methods Object.keys and Object.values to retrieve the keys and values of an object respectively:

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

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

You can also use the static method Object.entries to retrieve the key-value pairs of an object:

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

console.log(Object.entries(task));

This will output:

[
  ['id', 1],
  ['title', 'Read the Next.js book'],
  ['description', 'Read and understand the Next.js book.'],
];

The for...in Loop

The for...in loop allows you to iterate over the properties of an object. For example:

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

for (const prop in task) {
  console.log(prop);
}

This will output:

id
title
description

Maps

A Map object is a collection of a key-value pairs:

const capitals = new Map([
  ['Germany', 'Berlin'],
  ['France', 'Paris'],
]);
capitals.set('Spain', 'Madrid');
console.log(capitals.get('France')); // Paris
console.log(capitals.size); // 3
capitals.delete('France');
console.log(capitals.has('France')); // false

At first glance maps appear to be very similar to objects. However, there are a few important differences.

First, it's very easy to get the size of a map (using the size property), whereas with objects you would need to keep track of the size manually.

Second, the keys of an object are usually strings, whereas with maps they can have any data type.

It's recommended to use maps if the key-value pairs are unknown until run time (for example, because they are determined by user input), all keys have the same type and all values have the same type.

Sets

A Set object is a collection of unique values:

const values = new Set([1, 2, 3]);
values.add(4);
console.log(values.has(2)); // true
console.log(values.size); // 4
values.delete(3);
console.log(values.has(3)); // false

Note that all values in a set must be unique, i.e. duplicates are not allowed:

const values = new Set([1, 2, 3]);
values.add(2);
console.log(values); // Set(3) { 1, 2, 3 }

JSON

JSON is a data format for data exchange (e.g. on a network) and can basically store nested JavaScript objects and arrays. While it was heavily inspired by JavaScript, it has become a language-independent data format and is in fact used by many other programming languages to exchange data.

Here is an example JSON file:

{
  "user": {
    "name": "John Doe",
    "age": 24,
    "hobbies": ["running", "swimming"]
  },
  "group": "Example group"
}

Yes, this is exactly the same syntax that you would use in JavaScript to define objects and arrays.

You can convert a JavaScript value to a JSON string using JSON.stringify:

const result1 = JSON.stringify({ x: 1 });
console.log(typeof result1); // 'string'
console.log(result1); // {"x":1}

const result2 = JSON.stringify([1, 2, 3]);
console.log(typeof result2); // 'string'
console.log(result2); // [1,2,3]

Note that JSON.stringify has some unintuitive behaviors. For example, running JSON.stringify on a map or a set will always return {}:

console.log(JSON.stringify(new Map([[1, 2]]))); // {}
console.log(JSON.stringify(new Set([1, 2]))); // {}

The reverse operation to JSON.stringify is JSON.parse which takes a JSON string and constructs a JavaScript value from it:

const obj = JSON.parse('{"x": 1}');
console.log(typeof obj); // 'object'
console.log(obj); // { x: 1 }

const arr = JSON.parse('[1, 2, 3]');
console.log(typeof arr); // 'object'
console.log(arr); // [ 1, 2, 3 ]

We will revisit the JSON data format when we start sending data over a network.