Skip to content

JavaScript Foundations: Values, Functions, Async, and Tests

Step 6 of 45

JavaScript Functions, Array Methods, and Immutable Updates

Introduction

JavaScript functions and array methods make data ready for an interface. A common React task is a transformation of data: take a list of registrations, keep the paid ones, group them, sort them, and render the result. This lesson, part of the JavaScript domain of the Meta Front-End Developer pathway (course 2), gives you the tools for that work: functions, arrays, objects, the map/filter/reduce family, destructuring, spread and rest, and the habit of updating data without mutating it. React state updates depend on calling a state setter. If the next state is the same object as before under Object.is, React can skip the update; mutating an existing array and returning it is therefore unsafe.

The practice scenario is original: Harbor Workshops has a list of registrations and needs a sorted attendance summary for the organizer.

Learning objectives

  1. Write functions as declarations, expressions, and arrow functions, with default and rest parameters.
  2. Explain return values, undefined results, and the difference between pure and impure functions.
  3. Use map, filter, reduce, find, some, every, includes, and sort correctly.
  4. Read and write destructuring, spread, and rest syntax.
  5. Update arrays and objects immutably.
  6. Distinguish shallow copies from deep copies and know when each matters.
  7. Describe closures and the behavior of this at a basic level.

1. Functions

A function is a reusable block of code that receives parameters (names in the definition), is called with arguments (values at the call), and returns a value.

JavaScript
function seatsLeft(capacity, booked) {   // declaration
  return capacity - booked;
}

const seatsLeft2 = function (capacity, booked) { // expression
  return capacity - booked;
};

const seatsLeft3 = (capacity, booked) => capacity - booked; // arrow, implicit return

Key differences:

Form Hoisted? Own this? Typical use
Declaration Yes, fully Yes Named utilities
Function expression Function value created when the expression executes; binding availability depends on var/let/const Yes Callbacks, assigned values
Arrow function Created when its expression executes; a const binding is in the TDZ beforehand No (uses the surrounding this) Short callbacks, React code

An arrow function with a block body needs an explicit return. Without it the function returns undefined:

JavaScript
const double = (n) => { n * 2 };     // returns undefined!
const double2 = (n) => n * 2;        // returns the product
const makeObj = () => ({ ok: true });// parentheses needed to return an object literal

An ordinary non-async function called normally without a return statement returns undefined. Async functions instead return a promise, as the next lesson explains. Forgetting return inside a callback is a very common reason map produces [undefined, undefined].

Default and rest parameters

JavaScript
const add = (a, b = 1) => a + b;
console.log(add(2));            // 3  (b defaults to 1)
console.log(add(2, undefined)); // 3  (undefined triggers the default)
console.log(add(2, null));      // 2  (null does not: 2 + null = 2)

function sum(...xs) {            // rest: collects remaining arguments into an array
  return xs.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum());        // 0

Functions are values

Functions can be stored in variables, passed to other functions, and returned from them. A function that takes or returns a function is a higher-order function. Array methods like map are higher-order: you pass them a callback.

Closures

A closure is a function that remembers the variables of the place where it was created.

JavaScript
const makeCounter = () => {
  let n = 0;
  return () => ++n;
};
const counter = makeCounter();
counter();
console.log(counter()); // 2

n is private to the closure; nothing else can change it. React hooks and event handlers rely on this behavior, which is why a handler "sees" the values of the render in which it was created.

Pure functions

A pure function returns the same output for the same input and has no side effects (no changing outer variables, no writing to the DOM, no network). Pure functions are easy to test and are exactly what React expects from a component's rendering logic.

JavaScript
let total = 0;
const addImpure = (n) => { total += n; };    // impure: changes outer state
const addPure = (current, n) => current + n; // pure

2. Arrays

An array is an indexed collection starting at 0. For a dense array, length is one more than its last index. Sparse arrays can have empty slots, including trailing ones, so length is not a count of present elements.

JavaScript
const nums = [10, 2, 33, 4];
nums[0];        // 10
nums.at(-1);    // 4
nums.length;    // 4

Methods that mutate vs. methods that return new arrays

Mutates the original Returns a new array/value
push, pop, shift, unshift map, filter, slice, concat
splice, sort, reverse [...arr], flat, toSorted, toReversed

Before transforming data, identify which methods mutate their receiver. sort() and reverse() change the array they are called on.

For related practice, study JavaScript coercion and numeric conversions.

The transformation trio

map transforms present elements and returns an array of the same length, retaining empty slots in a sparse input:

JavaScript
["1", "2", "3"].map(Number); // [1, 2, 3]

filter keeps elements whose callback returns a truthy value:

JavaScript
[1, 2, 3, 4].filter((n) => n % 2 === 0); // [2, 4]

reduce folds the array into a single value. It takes a callback (accumulator, element) => newAccumulator and an initial value:

JavaScript
[1, 2, 3].reduce((acc, n) => acc + n, 0); // 6

Always provide the initial value. Without it, the first element is used as the accumulator, which fails on empty arrays (TypeError: Reduce of empty array with no initial value).

Other helpers worth knowing:

JavaScript
[1, 2, 3, 4].find((n) => n > 2);      // 3     first match or undefined
[1, 2, 3, 4].findIndex((n) => n > 9); // -1    not found
[1, 2, 3].some((n) => n > 2);         // true  at least one
[1, 2, 3].every((n) => n > 2);        // false all
[1, 2].includes(2);                   // true
[[1], [2, [3]]].flat();               // [1, 2, [3]] one level
Array.from({ length: 3 }, (_, i) => i * 2); // [0, 2, 4]

On an empty array, every() returns true and some() returns false; do not interpret every() as a test that at least one item exists.

A well-known trap: ["1", "2", "3"].map(parseInt) gives [1, NaN, NaN], because map passes (element, index, array) and parseInt reads the index as its radix. Write .map((s) => parseInt(s, 10)) or use Number.

Sorting

sort() without a comparator converts elements to strings and compares UTF-16 code-unit sequences, so numeric values do not get numeric order:

JavaScript
const nums = [10, 2, 33, 4];
console.log([...nums].sort());                 // [10, 2, 33, 4] (string order)
console.log([...nums].sort((a, b) => a - b));  // [2, 4, 10, 33]

The comparator returns a negative number if a goes first, positive if b goes first, and zero for a tie. Note the copy [...nums]: sorting the original would mutate it, which can break React state. toSorted() returns a sorted copy in current browsers and Node.

3. Objects

An object is a collection of key-value pairs.

JavaScript
const registration = {
  name: "Lena",
  workshop: "Pottery",
  seats: 2,
  paid: true,
  describe() { return `${this.name}: ${this.seats}`; }, // method shorthand
};

registration.name;        // dot access
registration["workshop"]; // bracket access, needed for dynamic keys
Object.keys(registration);   // ["name", "workshop", ...]
Object.values({ a: 1, b: 2 }); // [1, 2]
Object.entries({ a: 1 });      // [["a", 1]]
Object.fromEntries([["a", 1]]);// { a: 1 }

Computed property names let you build keys at runtime: { [workshop]: seats }.

this in short

For an ordinary method invoked as obj.method(), this is obj. If you extract the method (const f = obj.describe; f()), this is lost and the call fails or returns undefined values. Arrow functions do not have their own this; they use the one from the surrounding code. This is why arrow functions are convenient for callbacks and why you should not use them as object methods that rely on this.

4. Destructuring, spread, and rest

Destructuring unpacks values into variables:

JavaScript
const [first, , third = 9] = [1, 2];
console.log(first, third); // 1 9  (third uses its default)

const { name, ...rest } = registration;
console.log(name);  // "Lena"
console.log(rest);  // { workshop: "Pottery", seats: 2, paid: true, describe: [Function] }

You can destructure function parameters directly, which React components do constantly:

JavaScript
const label = ({ workshop, seats = 1 }) => `${workshop} x${seats}`;

Spread (...) expands an iterable or object in place:

JavaScript
const a = [1, 2, 3];
const copy = [...a, 4];            // [1, 2, 3, 4], a is unchanged
const merged = { ...registration, paid: false }; // later keys win

The same three dots mean rest when collecting (in parameters or the left side of destructuring) and spread when expanding (in calls, array literals, and object literals).

For related practice, study React events, state setters, and rendering.

5. Immutable updates

Assignment copies a value. When that value is an object reference, two names can refer to the same object. Function arguments are also passed by value; reassigning a parameter does not reassign the caller’s variable, while mutating the shared object remains visible:

JavaScript
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4]  a changed too

To change data without mutation, create a new value:

Goal Mutating Immutable
Add item arr.push(x) [...arr, x]
Remove item arr.splice(i, 1) arr.filter((_, idx) => idx !== i)
Replace item arr[i] = x arr.map((v, idx) => (idx === i ? x : v))
Change property obj.k = v { ...obj, k: v }
Sort arr.sort(fn) [...arr].sort(fn) or arr.toSorted(fn)

Updating one record in a list:

JavaScript
const users = [{ id: 1, n: "a" }, { id: 2, n: "b" }];
const updated = users.map((x) => (x.id === 2 ? { ...x, n: "B" } : x));
console.log(updated); // [{ id: 1, n: "a" }, { id: 2, n: "B" }]
console.log(users[1].n); // "b" (original untouched)

Shallow versus deep copies

Spread and slice make shallow copies: nested objects are still shared.

JavaScript
const o1 = { x: { y: 1 } };
const o2 = { ...o1 };
o2.x.y = 2;
console.log(o1.x.y); // 2  nested object shared!

structuredClone(o1) deep-copies supported data, including cycles, but is not a universal copy operation: functions and DOM nodes cannot be cloned and custom class behavior is not preserved. Unsupported values can cause DataCloneError. For React state, often update only the nested path you change: { ...o1, x: { ...o1.x, y: 2 } }. The second form is the pattern you will use with React state.

6. Worked example: the attendance summary

Task. Given the registrations below, produce one row per workshop with the number of paid seats, sorted by seats descending and then by workshop name.

JavaScript
const regs = [
  { name: "Lena", workshop: "Pottery", seats: 2, paid: true },
  { name: "Omar", workshop: "Weaving", seats: 1, paid: false },
  { name: "Ana",  workshop: "Pottery", seats: 3, paid: true },
  { name: "Ben",  workshop: "Weaving", seats: 4, paid: true },
];

Step 1: keep paid registrations. regs.filter((r) => r.paid) removes Omar.

Step 2: group and total. Use reduce with an object accumulator:

JavaScript
const summary = regs
  .filter((r) => r.paid)
  .reduce(
    (acc, r) => ({
      ...acc,
      [r.workshop]: (Object.hasOwn(acc, r.workshop) ? acc[r.workshop] : 0) + r.seats,
    }),
    {}
  );
console.log(summary); // { Pottery: 5, Weaving: 4 }

Trace: start {}; Lena gives { Pottery: 2 }; Ana gives { Pottery: 5 }; Ben gives { Pottery: 5, Weaving: 4 }. Object.hasOwn supplies zero for a new workshop while avoiding inherited names such as constructor. These sample registrations have already validated numeric seat counts. Repeatedly spreading an accumulator is clear for a small list but can be expensive for many distinct groups; a locally owned Map is another useful option.

Step 3: turn the object into rows and sort.

JavaScript
const rows = Object.entries(summary)
  .map(([workshop, seats]) => ({ workshop, seats }))
  .sort((a, b) => b.seats - a.seats || a.workshop.localeCompare(b.workshop, "en"));
console.log(rows);
// [{ workshop: "Pottery", seats: 5 }, { workshop: "Weaving", seats: 4 }]

The comparator uses ||: if the seat difference is 0 (falsy) it falls through to the name comparison.

Why other approaches are weaker. A for loop with summary[w] += seats works, but mutates and fails when the key does not exist yet (undefined + 2 is NaN) unless you guard it. Sorting Object.entries directly without a comparator would sort the pairs as strings. Filtering after grouping would count Omar's unpaid seat in the total.

7. Pitfalls and study tips

  • Pitfall: sort() with numbers and no comparator. Tip: if a question sorts [10, 2, 33, 4] with no callback, the answer is string order.
  • Pitfall: map callback with a block body and no return.
  • Pitfall: reduce without an initial value on possibly empty arrays.
  • Pitfall: .map(parseInt).
  • Pitfall: mutating state in place (push, sort, direct assignment). In React the screen may not update.
  • Pitfall: believing spread deep-copies.
  • Pitfall: returning an object literal from an arrow without parentheses; () => { ok: true } is a block with a label, returning undefined.
  • Tip: identify whether a method mutates before predicting what the original array contains afterwards.
  • Tip: const arr = []; arr.push(1) is legal; const does not freeze.

Mini case study: the waiting-list bug

Situation. A teammate wrote this to add a person to the waiting list and show the position of each person:

JavaScript
const waiting = ["Lena", "Omar"];

function addToWaiting(list, name) {
  list.push(name);
  return list;
}

const before = waiting;
const after = addToWaiting(waiting, "Ana");
console.log(before === after);      // true
console.log(before.length);         // 3
const labels = after.map((n, i) => { `${i + 1}. ${n}` });
console.log(labels);

The snippet itself is plain JavaScript and shows no state setter. In a React component, assume the returned array is passed back to its state setter: the same reference can cause the update to be skipped. Independently, the labels print as [undefined, undefined, undefined].

Questions. Explain both symptoms and rewrite the code.

Commented solution.

  1. list.push mutates the array that waiting points at, and the function returns the same reference, so before === after is true. Passing that same array to a React state setter can be skipped because Object.is(previous, next) is true. Merely creating a new array also is not enough: pass it to the setter, for example setWaiting(previous => addToWaiting(previous, "Ana")).
  2. The arrow function in map uses braces without return, so each call returns undefined.
JavaScript
const addToWaiting = (list, name) => [...list, name];

const waiting = ["Lena", "Omar"];
const after = addToWaiting(waiting, "Ana");
console.log(waiting.length);   // 2  original intact
console.log(after.length);     // 3
const labels = after.map((n, i) => `${i + 1}. ${n}`);
console.log(labels); // ["1. Lena", "2. Omar", "3. Ana"]

The new function is pure: equivalent inputs give equivalent results, and nothing outside is modified. A Jest test can check both the returned contents and the unchanged input; lesson 4 introduces those assertions.

Key terms

  • Parameter / argument: the name in a definition / the value passed in a call.
  • Callback: a function passed to another function.
  • Higher-order function: takes or returns a function.
  • Closure: a function with access to variables of its creation scope.
  • Pure function: deterministic, with no side effects.
  • Mutation: changing existing data in place.
  • Destructuring: unpacking values into variables.
  • Spread / rest: ... to expand / to collect.
  • Shallow copy: copies the top level only.

Summary checklist

  • I can write a function three ways and know the arrow-function return rules.
  • I use default and rest parameters correctly.
  • I know which array methods mutate.
  • I can write map, filter, and reduce (with an initial value) from memory.
  • I always give sort a comparator for numbers and copy before sorting.
  • I read and write destructuring and spread.
  • I update arrays and objects immutably, including one item inside a list.
  • I can explain shallow copies, nested immutable updates, and the limits of structuredClone.
  • I can explain a closure with a counter example.

Official sources