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

  1. What an array is and what it is not
  2. Five ways of creating an array
  3. Indexes, at() and negative indexes
  4. length: reading it, and what happens when you assign to it
  5. Holes: sparse arrays
  6. Array.isArray and why typeof is no use
  7. The table to keep at hand: mutate versus return
  8. Adding and removing at the ends: push, pop, shift, unshift
  9. splice: deleting, inserting and replacing
  10. slice: copying a chunk without touching the original
  11. concat and join
  12. Searching: indexOf, lastIndexOf, includes and their limit with objects
  13. reverse and a note on the immutable versions
  14. Nested arrays, flat and flatMap
  15. Worked example: a task's tags
  16. Common Mistakes and Tips
  17. Exercises
  18. Conclusion

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

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

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

  1. Indexes, at() and negative indexes

You 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];

  1. length: reading it, and what happens when you assign to it

length 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' ]

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

In 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

  1. Array.isArray and why typeof is no use

console.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'));   // false

typeof 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

  1. 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']"]

  1. Adding and removing at the ends: push, pop, shift, unshift

All 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

  1. splice: deleting, inserting and replacing

splice is the most versatile method and the easiest to get wrong. Its signature is:

array.splice(startIndex, howManyToDelete, ...elementsToInsert)

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), fresh holds the deleted elements. The modified array is still a.
  • 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".)

  1. slice: copying a chunk without touching the original

slice(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 safe

Telling 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).

  1. concat and join

concat 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);   // 5

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

It 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 #purchasing

Be careful with null and undefined inside the array: join turns them into an empty string, not into 'null'.

console.log(['a', null, 'b', undefined].join('-'));   // 'a--b-'

  1. Searching: indexOf, lastIndexOf, includes and their limit with objects

const 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'));                 // false

indexOf 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));       // 1

To 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)

  1. reverse and a note on the immutable versions

reverse() 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 copy

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

  1. Nested arrays, flat and flatMap

An 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 strings

flat() 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 level

flatMap(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.

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

That 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'];:

  1. Add 'Tote bags' at the back and print how many are in the queue.
  2. Serve the first order (take it off the front) and print it.
  3. Add 'Urgent reprint' at the front, because Marta has prioritized it.
  4. Print the last order in the queue without using length.
  5. 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:

  1. allTags(backlog), returning a flat array with every tag, repeats included.
  2. tasksWithTag(backlog, tag), returning an array with the ids of the tasks carrying that tag.
  3. tagsAreValid(backlog), returning true only if every task has tags as a genuine array (use Array.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 bags

A 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' ]  ✓ untouched

The 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' }]));     // false

Twelve 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

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved