This blog post is part of the series “Learning web development” – which teaches people who have never programmed how to create web apps with JavaScript.
To download the projects, go to the GitHub repository learning-web-dev-code
and follow the instructions there.
I’m interested in feedback! If there is something you don’t understand, please write a comment at the end of this page.
In this chapter, we’ll explore the data structure Map
(a class) which lets us translate (“map”) from an input value to an output value. We’ll use a Map to display text upside-down in a terminal!
Map
A Map is a collection of entries. Each entry consists of a key and a value. We can look up a value via its key. That is similar to how dictionaries work – e.g. English–Spanish dictionaries: Given a word in English, you can look up a word in Spanish. That’s why this data structure is often called dictionary.
const englishToSpanish = new Map([
['yes', 'sí'],
['no', 'no'],
['maybe', 'quizás'],
]);
We created a Map by invoking the class Map
. Its argument is an array of key-value pairs. Each pair is also stored in an array. Another option (that wasn’t used) would have been to represent a pair as follows.
{ key: 'yes', value 'sí' }
Using an array has the upside of being more compact and the downside of being less descriptive.
.size
of a Map Maps have a .size
– the number of their entries:
> englishToSpanish.size
3
Note that an array has a .length
because it is linear: The order of its elements matters. For a Map, on the other hand, the order of its entries does not matter. That’s why it has a .size
.
There can be at most one entry with a given key. .get()
lets us look up a value via its key. .set()
lets us change the value associated with a given key:
> englishToSpanish.get('maybe')
'quizás'
> englishToSpanish.set('maybe', 'tal vez');
> englishToSpanish.get('maybe')
'tal vez'
If there is no entry with a given key then .get()
returns undefined
:
> englishToSpanish.get('hello')
undefined
We can also add new entries via .set()
:
> englishToSpanish.set('hello', 'hola');
> englishToSpanish.get('hello')
'hola'
Method .has()
checks if a given key exists in a Map:
> englishToSpanish.has('yes')
true
> englishToSpanish.has('goodbye')
false
We can use objects as keys:
const objToStr = new Map([
[{}, 'empty object'],
]);
However, comparison of keys works similarly to ===
. Therefore, we can’t look up the value in the previous Map via a new empty object:
> objToStr.get({})
undefined
Therefore, objects are rarely good Map keys (which is a shame). However, if an object has the same identity then we can look up a value:
import * as assert from 'node:assert/strict';
const key = {};
const objToStr = new Map([
[key, 'empty object'],
]);
assert.equal(
objToStr.get(key), 'empty object'
);
Note that we used an assertion to demonstrate how .get()
works. When explaining JavaScript features, assertions are a nice alternative to console interactions. They have the benefit that we can put them next to multi-line code. From now on, I’ll often omit the import statement for assert
(in line 1).
??
operator If there is no result, JavaScript often uses the “non-value” undefined
. If we are not sure if a given value is undefined
or not, we can use the ??
operator to specify a default value for the former case:
> 'real value' ?? 'default'
'real value'
> undefined ?? 'default'
'default'
This is how we might use ??
in a function:
> const useDefault = x => x ?? 'no value';
undefined
> useDefault(123)
123
> useDefault(undefined)
'no value'
??
example The following code uses ??
to return the value '(not found)'
whenever it can’t find a word in the englishToSpanish
dictionary:
const englishToSpanish = new Map([
['yes', 'sí'],
['no', 'no'],
['maybe', 'quizás'],
]);
const lookUpWord = (word) => {
return englishToSpanish.get(word) ?? '(not found)'
};
assert.equal(
lookUpWord('no'), 'no'
);
assert.equal(
lookUpWord('mountain'), '(not found)'
);
.get()
returns undefined
if it can’t find anything. And the ??
operator makes it easy to specify a value to use in that case.
Map
example: counting characters countChars()
returns a Map that maps characters to numbers of occurrences.
function countChars(chars) {
const charCounts = new Map();
for (let ch of chars) { // (A)
ch = ch.toLowerCase(); // (B)
const prevCount = charCounts.get(ch) ?? 0; // (C)
charCounts.set(ch, prevCount + 1);
}
return charCounts;
}
assert.deepEqual(
countChars('AaBccc'),
new Map([
['a', 2],
['b', 1],
['c', 3],
])
);
Array.from()
, for-of
(line A) loops over code points not JavaScript characters.'a'
and 'A'
count as the same letter (line B).??
to specify zero as the previous count if there is no entry yet. After that, .set()
either updates an existing entry or creates a new one.for
loop The name of the for
loop seemingly implies that it is similar to the for-of
loop, but it is actually more closely related to the while
loop. This is its syntax:
for (<declaration>; <condition>; <action>) {
// Body
}
while
loops work).This is what an equivalent while
loop looks like:
<declaration>;
while (<condition>) {
// Body
<action>;
}
The following code demonstrates how the for
loop works:
const logArray = (arr) => {
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]);
}
};
i++
means the same as i += 1
..map()
and .filter()
Each of these method is invoked on an array and uses a function it receives as an argument in order to create a new array. That function is often named a callback.
array.map()
array.map()
visits each element of the input array, invokes its callback with it and pushes the result to the output array:
> [1, 2, 3].map(num => num * num)
[ 1, 4, 9 ]
> ['a', 'p'].map(name => `<${name}>`)
[ '<a>', '<p>' ]
Therefore, the following two functions are equivalent:
const twice1 = (arr) => arr.map(x => x + x);
const twice2 = (arr) => {
const result = [];
for (const x of arr) {
result.push(x + x);
}
return result;
};
This is a more practical example of using .map()
:
import * as assert from 'node:assert/strict';
const purchases = [
{ product: 'toothbrush', quantity: 1 },
{ product: 'egg', quantity: 4 },
{ product: 'pen', quantity: 2 },
];
const products = purchases.map(p => p.product);
assert.deepEqual(
products,
['toothbrush', 'egg', 'pen']
);
array.filter()
array.filter()
visits each element of the input array, invokes its callback with it and only pushes it to the output array if the callback returns true
:
> [0, -1, 4, -8, 7].filter(x => x < 0)
[ -1, -8 ]
> [0, -1, 4, -8, 7].filter(x => x === 0)
[ 0 ]
> [0, -1, 4, -8, 7].filter(x => x > 0)
[ 4, 7 ]
Therefore, the following two functions are equivalent:
const nonEmpty1 = (strs) => strs.filter(s => s.length > 0);
const nonEmpty2 = (strs) => {
const result = [];
for (const s of strs) {
if (s.length > 0) {
result.push(s);
}
}
return result;
};
This is a more practical example of using .filter()
:
import * as assert from 'node:assert/strict';
const purchases = [
{ product: 'toothbrush', quantity: 1 },
{ product: 'egg', quantity: 4 },
{ product: 'pen', quantity: 2 },
];
const multiPurchases = purchases.map(p => p.quantity > 1);
assert.deepEqual(
multiPurchases,
[
{ product: 'egg', quantity: 4 },
{ product: 'pen', quantity: 2 },
]
);
Map
vs. .map()
The data structure Map
and the array method .map()
have the same name because they both map from something to something. However, they are also quite different. Maybe Dictionary
would have been a better name for the data structure.
process.argv
The global variable process
has several properties that help with writing shell commands. One of the is process.argv
which contains an array with the current shell command and its arguments. Lets explore how it works via process-argv.js
:
console.log(process.argv);
If we invoke that module as follows:
cd learning-web-dev/learning-web-dev-code/projects/
node process-argv.js
Then its output will look similar to this:
[
'/usr/bin/node',
'/home/robin/learning-web-dev-code/projects/process-argv.js',
'first',
'second',
'two words'
]
Note that we need to quote text if it contains spaces. Otherwise, the text will be considered multiple arguments.
upside-down-text.js
This project converts text to its upside-down version by using appropriate, sometimes obscure, Unicode characters:
cd learning-web-dev/learning-web-dev-code/projects/
node upside-down-text.js "How are you?"
Output:
¿noʎ ǝɹɐ ʍoH
This is how we set up a Map that helps us with translating from a character to its upside-down version:
const dict = new Map();
const addToDict = (source, target) => {
const srcArr = Array.from(source);
const trgArr = Array.from(target);
if (srcArr.length !== trgArr.length) { // (A)
throw new Error();
}
for (let i = 0; i < srcArr.length; i++) {
dict.set(srcArr[i], trgArr[i]);
}
};
addToDict(
`ABCDEFGHIJKLMNOPQRSTUVWXYZ`,
`ⱯꓭƆꓷƎℲ⅁HIꓩꓘ⅂ꟽNOԀꝹꓤS⊥ՈɅ𐤵X⅄Z`,
);
addToDict(
`abcdefghijklmnopqrstuvwxyz`,
`ɐqɔpǝɟƃɥᴉɾʞꞁɯuodbɹsʇnʌʍxʎz`,
);
addToDict(
`0123456789`,
`0ІᘔƐᔭ59Ɫ86`,
);
addToDict(
`!"#$%&'()*+,-./:;<=>?@[\\]^_\`{|}~`,
`¡„#$%⅋,)(*+'-˙/:؛>=<¿@]\\[ᵥ‾\`}|{~`,
);
Instead of calling addToDict()
multiple times with smaller strings, we also could have called it just once, with two long strings. However, this way, the code is easier to understand.
In line A, we perform a safety check and throw an error if it fails.
The upside-down versions were assembled from these two sources:
upsidedown
by Christoph BurgmerThe following function uses dict
to create an upside-down string:
const upsideDown = (str) => {
return Array.from(str)
.reverse()
.map(
(srcChar) => {
return dict.get(srcChar) ?? srcChar;
}
).join('')
;
};
We perform the following steps:
str
into an array with code units.The array method .reverse()
reverses the order of an array and returns that array. In other words: In contrast to .map()
and .filter()
, it changes the array it is invoked on. However, that’s not a problem in the previous code because we create a new array and changing it is OK.
> ['a', 'b', 'c'].reverse()
[ 'c', 'b', 'a' ]
This following code receives input from the shell (line A) and sends output to it (line B).
// First shell argument
const arg = process.argv[2]; // (A)
console.log( // (B)
upsideDown(arg)
);
upside-down-text.js
.
encode-decode-text/encode-decode-text.html
as an inspiration.encode-decode-text/rot13.js
via a Map.
encode-decode-text/rot13_test.js
to ensure that everything still works correctly.