You have been using arrays since Module 1, but always as a list you pull things out of by index inside a for. That is taking advantage of maybe ten per cent of what an array can do. In this lesson you study the structure in depth: the five ways of creating one, what length exactly is and what happens if you assign to it, why typeof [] === 'object' and what to use instead, and above all the distinction that prevents the most errors in real JavaScript: methods that modify the original array versus methods that return a new one. The table you will build in section 7 is permanent reference material. The whole route rests on two concrete uses from the project: maintaining the task list of the Taller Nómada board and managing a task's tags, that field the canonical model defines as "an array of strings" and that you have barely touched so far.
Contents
- What an array is and what it is not
- Five ways of creating an array
- Indexes,
at()and negative indexes length: reading it, and what happens when you assign to it- Holes: sparse arrays
Array.isArrayand whytypeofis no use- The table to keep at hand: mutate versus return
- Adding and removing at the ends:
push,pop,shift,unshift splice: deleting, inserting and replacingslice: copying a chunk without touching the originalconcatandjoin- Searching:
indexOf,lastIndexOf,includesand their limit with objects reverseand a note on the immutable versions- Nested arrays,
flatandflatMap - Worked example: a task's tags
- Common Mistakes and Tips
- Exercises
- Conclusion
- What an array is and what it is not
An array is an ordered list of values, reachable by their position (index), which starts at 0.
const tags = ['space', 'design', 'furniture'];
console.log(tags[0]); // 'space'
console.log(tags[2]); // 'furniture'
console.log(tags[3]); // undefined ← it does not exist, but it does not failThree properties set an array apart from a plain object:
| Object | Array | |
|---|---|---|
| Key | A name (title, status) |
A numeric position (0, 1, 2…) |
| Order | Not guaranteed for arbitrary keys | Guaranteed and meaningful |
| Size | Counted with Object.keys(o).length |
array.length, maintained by the engine |
And a technical warning that explains many oddities: a JavaScript array is, underneath, an object with numeric keys and a special length property. That is why you can do this, even though you should not:
const odd = ['a', 'b'];
odd.author = 'Lucía'; // ✗ it works, but it is a property, not an element
console.log(odd.length); // 2 ← length only counts the indexes
console.log(odd); // [ 'a', 'b', author: 'Lucía' ]Practical rule: an array holds elements, never named properties. If you need names, use an object.
- Five ways of creating an array
// 1. Literal: the normal way, the one you will use 95% of the time
const tags = ['space', 'design'];
const empty = [];
// 2. new Array(): it has a famous trap
const three = new Array('a', 'b', 'c'); // [ 'a', 'b', 'c' ] ← several arguments: elements
const six = new Array(6); // [ <6 empty items> ] ← ONE number: a length!
console.log(six.length); // 6, but it is completely empty
// 3. Array.of(): fixes that trap, it always creates elements
console.log(Array.of(6)); // [ 6 ] ← an array holding the number 6
console.log(Array.of('a', 'b')); // [ 'a', 'b' ]
// 4. Array.from(): builds an array from something iterable or "array-like"
console.log(Array.from('carpentry')); // [ 'c','a','r','p','e','n','t','r','y' ]
console.log(Array.from({ length: 4 }, (_, i) => i + 1)); // [ 1, 2, 3, 4 ]
// 5. Array(n).fill(): the idiom for creating n slots with the same value
const hoursPerDay = Array(5).fill(0);
console.log(hoursPerDay); // [ 0, 0, 0, 0, 0 ]Two of them are worth dwelling on:
new Array(6) versus Array.of(6). With a single numeric argument, new Array takes it that you are asking for an empty array of that length. It is a historical inconsistency in the language and the reason Array.of exists. Avoid new Array except for the deliberate case of reserving a length.
Array.from with a mapping function. The second parameter is a function applied to each slot, with the value and the index. It is the idiomatic way of generating sequences:
// The next 5 working days from a date (simplified version using strings)
const monthDays = Array.from({ length: 5 }, (_, i) => `2026-09-${20 + i}`);
console.log(monthDays);
// [ '2026-09-20', '2026-09-21', '2026-09-22', '2026-09-23', '2026-09-24' ]The _ as the name of the first parameter is a convention: it means "this argument exists but I do not use it".
Be careful with fill and objects, because it shares the reference (remember 01-05):
const rows = Array(3).fill({ hours: 0 });
rows[0].hours = 8;
console.log(rows); // [ { hours: 8 }, { hours: 8 }, { hours: 8 } ] ✗ the same object!
const good = Array.from({ length: 3 }, () => ({ hours: 0 }));
good[0].hours = 8;
console.log(good); // [ { hours: 8 }, { hours: 0 }, { hours: 0 } ] ✓fill copies the same value three times; if that value is a reference, all three slots point to the same object. Array.from with a function calls the function three times and creates three different objects.
- Indexes,
at() and negative indexes
at() and negative indexesYou already know the classic bracket access. What you may not know is at(), which does the same thing but accepts negative indexes, counting from the end:
const tags = ['space', 'design', 'furniture', 'budget'];
console.log(tags[0]); // 'space'
console.log(tags.at(0)); // 'space'
console.log(tags.at(-1)); // 'budget' ← the last one
console.log(tags.at(-2)); // 'furniture'
console.log(tags[-1]); // undefined ✗ careful! it looks for a property called "-1"Before at(), the last element was obtained with tags[tags.length - 1], which works but repeats the array's name and is noisier. When the array is the result of a long expression, the difference shows:
const lastTask = board.tasks.at(-1);
// versus
const lastTaskLongWay = board.tasks[board.tasks.length - 1];
length: reading it, and what happens when you assign to it
length: reading it, and what happens when you assign to itlength is not the number of elements: it is the highest index used, plus one. And, unlike almost any other language, it can be written to.
const tags = ['space', 'design', 'furniture'];
console.log(tags.length); // 3
// Assigning a SMALLER length truncates the array (it destroys elements)
tags.length = 2;
console.log(tags); // [ 'space', 'design' ]
// Assigning a LARGER length creates holes
tags.length = 5;
console.log(tags); // [ 'space', 'design', <3 empty items> ]
// The idiom for emptying an array without changing the reference
tags.length = 0;
console.log(tags); // []That last line is a useful trick: tags = [] would create a new array (and with const it would fail), whereas tags.length = 0 empties the one that already exists, so any other variable pointing at it will also see the empty array. It is exactly the difference between reassigning and mutating that you studied with references.
And be careful about assigning a very high index:
const tasks = ['a', 'b'];
tasks[9] = 'j';
console.log(tasks.length); // 10
console.log(tasks); // [ 'a', 'b', <7 empty items>, 'j' ]
- Holes: sparse arrays
An array with slots that have never been assigned is called sparse (scattered, full of holes). The holes are not undefined: they are the absence of the property, and some methods skip them.
const withHole = [1, , 3]; // the double comma leaves a hole at position 1
console.log(withHole.length); // 3
console.log(withHole[1]); // undefined ← reading it, it looks like undefined
console.log(1 in withHole); // false ← but the key does not exist
console.log(Object.keys(withHole)); // [ '0', '2' ]
const withUndefined = [1, undefined, 3];
console.log(1 in withUndefined); // true ← here the key does existIn practice: avoid holes. They almost always appear by accident (delete tasks[2], which removes the element but leaves the hole, or new Array(6)), and they make some methods ignore them and others not, which produces bugs that are hard to see. If you want to remove an element for real, use splice (section 9), not delete:
const tasks = ['a', 'b', 'c'];
delete tasks[1];
console.log(tasks); // [ 'a', <1 empty item>, 'c' ] ✗ length is still 3
const tasks2 = ['a', 'b', 'c'];
tasks2.splice(1, 1);
console.log(tasks2); // [ 'a', 'c' ] ✓ length is now 2
Array.isArray and why typeof is no use
Array.isArray and why typeof is no useconsole.log(typeof []); // 'object' ✗ useless
console.log(typeof {}); // 'object'
console.log(typeof null); // 'object' (the historical bug from 01-05)
console.log(Array.isArray([])); // true ✓
console.log(Array.isArray({})); // false
console.log(Array.isArray('space')); // falsetypeof does not tell arrays apart from objects because, as we saw, an array is an object. Array.isArray is the correct check, and it is especially useful when validating model data, where tags must always be an array:
function validateTags(task) {
if (!Array.isArray(task.tags)) {
throw new Error(`The tags of task ${task.id} must be an array.`);
}
return true;
}
validateTags({ id: 1, tags: ['space'] }); // true
// validateTags({ id: 8, tags: 'space' }); // ✗ Error
- The table to keep at hand: mutate versus return
This is the most important section of the lesson. Array methods split into two families, and confusing them is the number one cause of bugs with lists:
- Those that mutate: they modify the array they are called on. They usually return something else (the new
length, the removed element…) or the array itself. - Those that return a new one: they leave the original untouched and produce a different array (or value).
| Method | Does it mutate the original? | What it returns |
|---|---|---|
push(...v) |
Yes | The new length |
pop() |
Yes | The removed element |
shift() |
Yes | The first element, removed |
unshift(...v) |
Yes | The new length |
splice(i, n, ...v) |
Yes | An array with the removed elements |
reverse() |
Yes | The same array, now reversed |
sort(cmp) |
Yes (lesson 04-05) | The same array, now sorted |
fill(v) |
Yes | The same array |
slice(i, j) |
No | A new array with the chunk |
concat(...) |
No | A new array with everything joined |
join(sep) |
No | A string |
indexOf / lastIndexOf / includes |
No | A number / a boolean |
at(i) |
No | The element |
flat() / flatMap(f) |
No | A new array |
map / filter (04-04, 04-05) |
No | A new array |
toSorted / toReversed (04-05) |
No | A new array |
A fairly reliable mnemonic: the methods that mutate are named after an action performed on the list (push, pop, splice, reverse, sort, fill); the ones that return something new are named after producing something (slice, concat, join, flat, map). And a style rule: in Nómada Tasks we prefer the non-mutating methods whenever possible, because an array nobody modifies behind your back is an array you can trust. In 04-07 you will see how to build complete immutable updates.
flowchart TD
A["const original = ['a','b','c']"] --> B{"Which method?"}
B -->|"original.push('d')"| C["original = ['a','b','c','d']<br/>returns 4"]
B -->|"original.slice(0, 2)"| D["original IS STILL ['a','b','c']<br/>returns ['a','b']"]
- Adding and removing at the ends:
push, pop, shift, unshift
push, pop, shift, unshiftAll four mutate. They differ by which end and in which direction:
| Method | End | Action | Returns |
|---|---|---|---|
push(v) |
Back | Adds | New length |
pop() |
Back | Removes | The removed element |
unshift(v) |
Front | Adds | New length |
shift() |
Front | Removes | The removed element |
const tags = ['space', 'design'];
console.log(tags.push('furniture')); // 3 ← it returns the length, not the array
console.log(tags); // [ 'space', 'design', 'furniture' ]
console.log(tags.push('budget', 'works')); // 5 ← it accepts several at once
const last = tags.pop();
console.log(last); // 'works'
console.log(tags.length); // 4
tags.unshift('urgent');
console.log(tags);
// [ 'urgent', 'space', 'design', 'furniture', 'budget' ]
const first = tags.shift();
console.log(first); // 'urgent'A performance warning that will matter in Module 9: push and pop work at the back and are very fast. shift and unshift work at the front and force the engine to reindex every element, so on large lists they are noticeably more expensive. With the six Taller Nómada tasks it makes no difference; with a hundred thousand records, it does.
Applied to the board:
const board = { tasks: [] };
function addTask(board, task) {
board.tasks.push(task);
return board.tasks.length;
}
console.log(addTask(board, { id: 1, title: 'Redesign the multipurpose room' })); // 1
console.log(addTask(board, { id: 2, title: 'Signage for the screen-printing workshop' })); // 2
splice: deleting, inserting and replacing
splice: deleting, inserting and replacingsplice is the most versatile method and the easiest to get wrong. Its signature is:
Its three uses, each starting from the same array:
// USE 1 — DELETE: second argument > 0, no elements to insert
const a = ['space', 'design', 'furniture', 'budget'];
const removed = a.splice(1, 2); // from index 1, delete 2
console.log(removed); // [ 'design', 'furniture' ] ← what was removed
console.log(a); // [ 'space', 'budget' ]
// USE 2 — INSERT: second argument 0, plus the new elements
const b = ['space', 'budget'];
b.splice(1, 0, 'design', 'furniture'); // at index 1, delete 0, insert 2
console.log(b); // [ 'space', 'design', 'furniture', 'budget' ]
// USE 3 — REPLACE: delete and insert at once
const c = ['space', 'design', 'furniture'];
c.splice(1, 1, 'interior-design'); // at index 1, delete 1, insert 1
console.log(c); // [ 'space', 'interior-design', 'furniture' ]Details that avoid surprises:
- It returns what was removed, not the resulting array. If you write
const fresh = a.splice(1, 2),freshholds the deleted elements. The modified array is stilla. - If you leave out the second argument, it deletes to the end:
a.splice(2)removes everything from index 2 onwards. - It accepts negative indexes:
a.splice(-1, 1)deletes the last element.
Applied to the project, removing a task from the board by its id:
function removeTask(board, id) {
for (let i = 0; i < board.tasks.length; i++) {
if (board.tasks[i].id === id) {
const [removed] = board.tasks.splice(i, 1);
return removed;
}
}
return null;
}(The line const [removed] = ... is array destructuring: it takes the first element of the array splice returns. It is the subject of 04-06; for now read it as "the first of whatever was removed".)
slice: copying a chunk without touching the original
slice: copying a chunk without touching the originalslice(start, end) returns a new array with the elements from start up to end, not including end. It does not touch the original.
const tasks = ['T1', 'T2', 'T3', 'T4', 'T5', 'T6'];
console.log(tasks.slice(0, 3)); // [ 'T1', 'T2', 'T3' ] ← the 3 is NOT included
console.log(tasks.slice(3)); // [ 'T4', 'T5', 'T6' ] ← from 3 to the end
console.log(tasks.slice(-2)); // [ 'T5', 'T6' ] ← the last two
console.log(tasks.slice()); // a full copy
console.log(tasks); // untouched ✓slice() with no arguments is the classic idiom for copying an array, and it is what lets you sort without destroying the original:
const copy = tasks.slice();
copy.reverse(); // reverse mutates... but only the copy
console.log(tasks[0]); // 'T1' ← the original is safeTelling slice from splice is essential. The names look alike; the behavior is the opposite:
slice(i, j) |
splice(i, n, ...) |
|
|---|---|---|
| Does it mutate? | No | Yes |
| Second argument | The end index (excluded) | How many to delete |
| Returns | The copied chunk | What was removed |
| Useful for | Copying, paginating | Deleting, inserting, replacing |
Memory trick: splice has a p for "peril" (it mutates).
concat and join
concat and joinconcat joins arrays and returns a new one:
const ivanTasks = ['Redesign the multipurpose room', 'Bookbinding guide for residents'];
const martaTasks = ['Signage for the screen-printing workshop'];
const allTasks = ivanTasks.concat(martaTasks);
console.log(allTasks.length); // 3
console.log(ivanTasks.length); // 2 ← untouched
// It accepts several arrays and also loose values
console.log(ivanTasks.concat(martaTasks, ['Carpentry workshop quote'], 'Extra').length); // 5In modern code, the spread operator from 04-07 replaces concat almost always ([...ivanTasks, ...martaTasks]), but concat is still perfectly valid and turns up in a lot of existing code.
join turns an array into a string, joining the elements with the separator you give it (by default, a comma):
const tags = ['carpentry', 'purchasing'];
console.log(tags.join()); // 'carpentry,purchasing'
console.log(tags.join(', ')); // 'carpentry, purchasing'
console.log(tags.join(' · ')); // 'carpentry · purchasing'
console.log([].join(', ')); // '' ← empty array: empty stringIt is exactly what you need to print a task's tags on a report line:
function lineWithTags(task) {
const tags = task.tags.length > 0
? ` #${task.tags.join(' #')}`
: '';
return `[${task.id}] ${task.title}${tags}`;
}
console.log(lineWithTags({ id: 6, title: 'Carpentry workshop quote',
tags: ['carpentry', 'purchasing'] }));
// [6] Carpentry workshop quote #carpentry #purchasingBe careful with null and undefined inside the array: join turns them into an empty string, not into 'null'.
- Searching:
indexOf, lastIndexOf, includes and their limit with objects
indexOf, lastIndexOf, includes and their limit with objectsconst tags = ['screen-printing', 'purchasing', 'screen-printing', 'storeroom'];
console.log(tags.indexOf('purchasing')); // 1
console.log(tags.indexOf('screen-printing')); // 0 ← the FIRST occurrence
console.log(tags.lastIndexOf('screen-printing')); // 2 ← the LAST one
console.log(tags.indexOf('web')); // -1 ← not there
console.log(tags.includes('storeroom')); // true
console.log(tags.includes('web')); // falseindexOf returns -1 when it finds nothing, which forces you to write awkward checks. includes (ES2016) returns a boolean directly and is what you should use when all you care about is whether it is there:
// Old, still widely seen
if (tags.indexOf('purchasing') !== -1) { }
// Modern and readable
if (tags.includes('purchasing')) { }Now, the important limit. Both compare with ===, so they work with primitives but not with objects, because two different objects are never equal even if they hold the same contents (01-07):
const tasks = [
{ id: 1, title: 'Redesign the multipurpose room' },
{ id: 6, title: 'Carpentry workshop quote' }
];
console.log(tasks.includes({ id: 6, title: 'Carpentry workshop quote' })); // false ✗
// With the SAME reference it does work
const reference = tasks[1];
console.log(tasks.includes(reference)); // true ✓
console.log(tasks.indexOf(reference)); // 1To search for objects by content you need find and findIndex, which take a comparison function. They are the opening of the lesson Searching, Sorting and Aggregating Data.
One last oddity worth knowing: includes does find NaN, and indexOf does not.
console.log([NaN].includes(NaN)); // true
console.log([NaN].indexOf(NaN)); // -1 ← because NaN !== NaN (01-07)
reverse and a note on the immutable versions
reverse and a note on the immutable versionsreverse() reverses the array in place: it mutates.
const order = ['pending', 'in-progress', 'done'];
const returned = order.reverse();
console.log(order); // [ 'done', 'in-progress', 'pending' ] ← changed!
console.log(returned === order); // true ← it returns the SAME array, not a copyThat true is the trap: since reverse returns the array, it looks as though you are creating a new one, and you are not. If you need to keep the original, copy first:
const order2 = ['pending', 'in-progress', 'done'];
const reversed = order2.slice().reverse();
console.log(order2); // [ 'pending', 'in-progress', 'done' ] ✓ untouched
console.log(reversed); // [ 'done', 'in-progress', 'pending' ]Modern JavaScript adds toReversed(), which does exactly that without the manual copy. You will see it alongside toSorted() in 04-05, where the mutation problem becomes truly painful.
- Nested arrays,
flat and flatMap
flat and flatMapAn array can contain arrays. That is what happens, for example, if you collect the tags of every task:
const tagsPerTask = [
['space', 'design'],
['screen-printing', 'communication'],
['web', 'bookings']
];
console.log(tagsPerTask[1][0]); // 'screen-printing'
console.log(tagsPerTask.length); // 3 ← three arrays, not six stringsflat() flattens one level; with an argument, it flattens as many as you say:
console.log(tagsPerTask.flat());
// [ 'space', 'design', 'screen-printing', 'communication', 'web', 'bookings' ]
const deep = [1, [2, [3, [4]]]];
console.log(deep.flat()); // [ 1, 2, [ 3, [ 4 ] ] ] ← a single level
console.log(deep.flat(2)); // [ 1, 2, 3, [ 4 ] ]
console.log(deep.flat(Infinity)); // [ 1, 2, 3, 4 ] ← every levelflatMap(f) transforms and flattens one level in a single pass. It is the perfect shortcut for "a list with every tag in the backlog":
const backlog = [
{ id: 1, tags: ['space', 'design'] },
{ id: 2, tags: ['screen-printing', 'communication'] },
{ id: 4, tags: [] }
];
console.log(backlog.flatMap((t) => t.tags));
// [ 'space', 'design', 'screen-printing', 'communication' ]Notice that the task with no tags simply contributes nothing: flatMap with an empty array is also the idiomatic way of "transforming and discarding at the same time". The details of map as a transformation arrive in the next lesson.
- Worked example: a task's tags
Let us put it all together in a small set of functions for managing the tags field, respecting the project's rule R9 (lowercase and no duplicates). All of them return a new array: they do not mutate the task.
'use strict';
function normalize(tag) {
return tag.trim().toLowerCase();
}
/** Adds a tag if it was not there. Returns a NEW array. */
function withTag(tags, fresh) {
const clean = normalize(fresh);
if (clean === '') return tags.slice();
if (tags.includes(clean)) return tags.slice();
return tags.concat(clean);
}
/** Removes a tag if it was there. Returns a NEW array. */
function withoutTag(tags, target) {
const clean = normalize(target);
const copy = tags.slice();
const position = copy.indexOf(clean);
if (position !== -1) copy.splice(position, 1);
return copy;
}
/** Replaces one tag with another, keeping its position. */
function renameTag(tags, oldTag, newTag) {
const copy = tags.slice();
const position = copy.indexOf(normalize(oldTag));
if (position !== -1) copy.splice(position, 1, normalize(newTag));
return copy;
}
const originalTags = ['carpentry', 'purchasing'];
console.log(withTag(originalTags, ' URGENT ')); // [ 'carpentry', 'purchasing', 'urgent' ]
console.log(withTag(originalTags, 'Purchasing')); // [ 'carpentry', 'purchasing' ] ← already there
console.log(withoutTag(originalTags, 'purchasing')); // [ 'carpentry' ]
console.log(renameTag(originalTags, 'purchasing', 'Quotes'));
// [ 'carpentry', 'quotes' ]
console.log(originalTags); // [ 'carpentry', 'purchasing' ] ✓ untouched in all four callsThat last line is the goal of the whole design: the starting array never changes. Each function returns a new version, exactly as priorityWeight returned a number without touching anything. It is the same "pure function" idea from 03-03, now applied to lists.
And here is a comparison between the two strategies, so you can see the price of each:
// MUTATING: fast, but anyone holding the array sees the change
function addTagMutating(task, fresh) {
task.tags.push(normalize(fresh));
}
// IMMUTABLE: it creates a copy, but nobody gets a surprise
function addTagWithoutMutating(task, fresh) {
return { ...task, tags: withTag(task.tags, fresh) };
}(The { ...task, ... } in the second one is object spread, which you will study in 04-07. Make a mental note: it is the missing piece that completes the pattern.)
Common Mistakes and Tips
1. Expecting push to return the array. It returns the new length. const list = tasks.push(x) leaves list holding a number.
2. Confusing slice with splice. slice copies and does not mutate; splice cuts and mutates. If the result looks odd, check which one you wrote.
3. Using delete to remove an element. It leaves a hole and does not change length. Use splice(i, 1).
4. Searching for objects with includes or indexOf. They compare by reference, not by content. Use find/findIndex (04-05).
5. Modifying an array while walking it with indexes. If you splice inside an ascending for, the indexes shift and you skip elements. Two solutions: walk it backwards, or build a new array with filter.
const tasks = ['a', 'b', 'b', 'c'];
// ✗ It skips the second 'b'
for (let i = 0; i < tasks.length; i++) {
if (tasks[i] === 'b') tasks.splice(i, 1);
}
console.log(tasks); // [ 'a', 'b', 'c' ]
// ✓ Backwards, the pending indexes do not move
const tasks2 = ['a', 'b', 'b', 'c'];
for (let i = tasks2.length - 1; i >= 0; i--) {
if (tasks2[i] === 'b') tasks2.splice(i, 1);
}
console.log(tasks2); // [ 'a', 'c' ]6. new Array(3) when you wanted [3]. Use Array.of(3) or simply the literal.
7. Storing named properties on an array. tasks.total = 6 is legal and is nearly always a design mistake: use an object { tasks: [...], total: 6 }.
Professional tip. Faced with any array method you cannot remember, ask yourself one question before writing it: does it mutate or does it return? If it mutates and you did not want to mutate, put .slice() in front. That habit —copy before modifying— will save you more debugging hours than any other habit in this module.
Exercises
Exercise 1 — The workshop queue. The screen-printing workshop takes orders in the order they arrive. Starting from const queue = ['Workshop signage', 'Course T-shirts'];:
- Add
'Tote bags'at the back and print how many are in the queue. - Serve the first order (take it off the front) and print it.
- Add
'Urgent reprint'at the front, because Marta has prioritized it. - Print the last order in the queue without using
length. - Print the queue as a string separated by
' → '.
Exercise 2 — Reordering the task list. Write a function moveTask(tasks, from, to) that returns a new array with the task at position from placed at position to, without mutating the original. Try it by moving the task at index 0 to index 2 in ['T1','T2','T3','T4'], and check that the original array has not changed.
Exercise 3 — Tag audit. Given the canonical backlog (with its id and tags fields), write:
allTags(backlog), returning a flat array with every tag, repeats included.tasksWithTag(backlog, tag), returning an array with theids of the tasks carrying that tag.tagsAreValid(backlog), returningtrueonly if every task hastagsas a genuine array (useArray.isArray).
Solutions
Exercise 1
const queue = ['Workshop signage', 'Course T-shirts'];
// 1. push returns the new length
console.log(queue.push('Tote bags')); // 3
// 2. shift takes from the front and returns the element
const served = queue.shift();
console.log(served); // 'Workshop signage'
// 3. unshift adds at the front
queue.unshift('Urgent reprint');
console.log(queue);
// [ 'Urgent reprint', 'Course T-shirts', 'Tote bags' ]
// 4. at(-1) without resorting to length
console.log(queue.at(-1)); // 'Tote bags'
// 5. join with a separator
console.log(queue.join(' → '));
// Urgent reprint → Course T-shirts → Tote bagsA real queue (first in, first out) is implemented like this: push to enter and shift to leave. If instead of a queue you wanted a stack (last in, first out), it would be push and pop.
Exercise 2
function moveTask(tasks, from, to) {
if (from < 0 || from >= tasks.length) {
throw new Error(`Invalid source position: ${from}`);
}
const copy = tasks.slice(); // 1. copy so as not to mutate
const [moved] = copy.splice(from, 1); // 2. take it out of its place
copy.splice(to, 0, moved); // 3. insert it at the destination
return copy;
}
const original = ['T1', 'T2', 'T3', 'T4'];
const movedList = moveTask(original, 0, 2);
console.log(movedList); // [ 'T2', 'T3', 'T1', 'T4' ]
console.log(original); // [ 'T1', 'T2', 'T3', 'T4' ] ✓ untouchedThe three lines of the body sum up the whole lesson: copy with slice, remove with splice(i, 1) taking advantage of the fact that it returns what was removed, and insert with splice(i, 0, v). A common mistake is to forget the initial slice: the function would still work, but it would mutate the caller's list, which is exactly what we wanted to avoid.
Exercise 3
const backlog = [
{ id: 1, tags: ['space', 'design'] },
{ id: 2, tags: ['screen-printing', 'communication'] },
{ id: 3, tags: ['web', 'bookings'] },
{ id: 4, tags: ['screen-printing', 'storeroom'] },
{ id: 5, tags: ['bookbinding', 'documentation'] },
{ id: 6, tags: ['carpentry', 'purchasing'] }
];
function allTags(backlog) {
return backlog.flatMap((task) => task.tags);
}
function tasksWithTag(backlog, tag) {
const ids = [];
for (const task of backlog) {
if (task.tags.includes(tag)) ids.push(task.id);
}
return ids;
}
function tagsAreValid(backlog) {
for (const task of backlog) {
if (!Array.isArray(task.tags)) return false;
}
return true;
}
console.log(allTags(backlog).length); // 12
console.log(tasksWithTag(backlog, 'screen-printing')); // [ 2, 4 ]
console.log(tasksWithTag(backlog, 'web')); // [ 3 ]
console.log(tagsAreValid(backlog)); // true
console.log(tagsAreValid([{ id: 9, tags: 'web' }])); // falseTwelve tags in total, but only eleven distinct ones: 'screen-printing' appears in tasks 2 and 4. Deduplicating them with a Set is one of the examples in 04-05. And notice tagsAreValid: it returns false as soon as it finds a failure, without walking the rest. It is the early-exit pattern from 02-04, and it is exactly what the every method you will study in lesson 04-05 does.
Conclusion
You now understand the array as a structure, not just as "the thing you walk with a for". You know how to create one in five ways and why new Array(6) is not the same as Array.of(6); you know that length is writable and that assigning 0 to it is the idiom for emptying a list without changing its reference; you know what holes are and why they are best avoided; and you know that typeof [] is no use whatsoever, whereas Array.isArray answers exactly what you asked.
Above all, you have the mutate-versus-return table, which is the compass for all work with lists: push, pop, shift, unshift, splice, reverse, sort and fill modify the original; slice, concat, join, at, flat, flatMap, includes and indexOf do not. You have seen splice in its three uses —deleting, inserting and replacing—, you have told slice apart from splice, you have learned that includes and indexOf compare with === and therefore fail with objects, and you have applied all of it to the project's tags with a set of functions that never mutate the original task, plus the board's moveTask and removeTask functions.
One thing remains unresolved, and it is precisely the one that recurs in almost all the code in this lesson: the hand-written loops. tasksWithTag walks with for...of and keeps pushing into a helper array; tagsAreValid walks in order to bail out as soon as it finds a failure; removeTask walks with indexes to locate a position. Walking a list is so frequent that JavaScript offers six different ways of doing it, each with its own rules about the index, about whether it can be interrupted and about what it returns. Which one to choose in each case is the subject of Iterating over Arrays, where you will also meet map again, this time as the native method rather than the hand-rolled version you wrote in 03-06.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
