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
- Write functions as declarations, expressions, and arrow functions, with default and rest parameters.
- Explain return values,
undefinedresults, and the difference between pure and impure functions. - Use
map,filter,reduce,find,some,every,includes, andsortcorrectly. - Read and write destructuring, spread, and rest syntax.
- Update arrays and objects immutably.
- Distinguish shallow copies from deep copies and know when each matters.
- Describe closures and the behavior of
thisat 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.
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 returnKey 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:
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 literalAn 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
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()); // 0Functions 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.
const makeCounter = () => {
let n = 0;
return () => ++n;
};
const counter = makeCounter();
counter();
console.log(counter()); // 2n 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.
let total = 0;
const addImpure = (n) => { total += n; }; // impure: changes outer state
const addPure = (current, n) => current + n; // pure2. 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.
const nums = [10, 2, 33, 4];
nums[0]; // 10
nums.at(-1); // 4
nums.length; // 4Methods 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:
["1", "2", "3"].map(Number); // [1, 2, 3]filter keeps elements whose callback returns a truthy value:
[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:
[1, 2, 3].reduce((acc, n) => acc + n, 0); // 6Always 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:
[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:
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.
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:
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:
const label = ({ workshop, seats = 1 }) => `${workshop} x${seats}`;Spread (...) expands an iterable or object in place:
const a = [1, 2, 3];
const copy = [...a, 4]; // [1, 2, 3, 4], a is unchanged
const merged = { ...registration, paid: false }; // later keys winThe 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:
const a = [1, 2, 3];
const b = a;
b.push(4);
console.log(a); // [1, 2, 3, 4] a changed tooTo 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:
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.
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.
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:
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.
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:
mapcallback with a block body and noreturn. - Pitfall:
reducewithout 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, returningundefined. - Tip: identify whether a method mutates before predicting what the original array contains afterwards.
- Tip:
const arr = []; arr.push(1)is legal;constdoes 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:
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.
list.pushmutates the array thatwaitingpoints at, and the function returns the same reference, sobefore === afteristrue. Passing that same array to a React state setter can be skipped becauseObject.is(previous, next)is true. Merely creating a new array also is not enough: pass it to the setter, for examplesetWaiting(previous => addToWaiting(previous, "Ana")).- The arrow function in
mapuses braces withoutreturn, so each call returnsundefined.
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
returnrules. - I use default and rest parameters correctly.
- I know which array methods mutate.
- I can write
map,filter, andreduce(with an initial value) from memory. - I always give
sorta 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
- Meta Front-End Developer Professional Certificate (opens in a new tab)
- Course 2: Programming with JavaScript (opens in a new tab)
- MDN: Functions (opens in a new tab)
- MDN: Array (opens in a new tab)
- MDN: Destructuring assignment (opens in a new tab)
- MDN: Spread syntax (opens in a new tab)
- MDN: Closures (opens in a new tab)
- React: Updating arrays in state (opens in a new tab)
- React: State setter identity comparisons (opens in a new tab)
- MDN: Array sorting (opens in a new tab)
- MDN: Structured clone limitations (opens in a new tab)
