3.7 Array Methods

Aligned outcomes:

SLO 2

Describe the principles of structured programming.

These methods build a new array rather than editing the old one, which is the data-level form of the isolation you just met: .map(), .slice() and .concat() all leave their input intact, so nothing downstream is surprised by a change it did not ask for.

SLO 3

Describe, design, implement, and test structured programs using currently accepted methodology.

Spread syntax and .map() are current practice, not just alternatives to a loop — a transformation written as one expression is shorter to read and to check. Recognizing ...name in a signature is what lets you read library documentation on your own.

Learning Objectives

By the end of this section you should be able to:

In this section, you will learn to:
  • Build a new array from an existing one with .map().
  • Copy part of an array with .slice(), and explain why it does not change the original.
  • Join arrays with .concat().
  • Spread an array into a function call's arguments.
  • Spread an array or object into a new one, overriding selected values.
  • Recognize ...name in a function signature when reading library documentation.

3.7.1 Transforming a List with .map()

Section 3.3 gave you arrays and the methods that change them in place — push, pop, shift, unshift. This section is about the other kind: methods that leave the original alone and hand you back something new.

Here is a job you can already do:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[10.8,21.6,32.400000000000006]

Four lines of loop to say one thing: every price, times 1.08. .map() says exactly that:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[10.8,21.6,32.400000000000006]
[10,20,30]

.map() calls your arrow once for each item, collects what each call returns, and gives back a new array of the results. The original prices is untouched — printed on the last line, still the numbers you started with.

Definition 3.7.1: .map()

.map(callback) builds a new array by calling callback once for each element of the original and collecting the returned values. The original array is not changed, and the new array always has the same length.

This is countMatching from Section 3.4.5 taken to its conclusion. There, you wrote the loop and supplied the rule as a callback. .map() is that arrangement built into the language: the method knows how to walk the array, and your arrow knows what to do with each item.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

["MARISOL","DEV","PRIYA"]
[7,3,5]
["Hi marisol!","Hi dev!","Hi priya!"]

.map() always returns an array of the same length. If you find yourself wanting fewer items than you started with, .map() is the wrong tool — that is a different job with a different method (Section 3.7.6). A .map() whose callback sometimes returns nothing produces an array padded with undefined, which is almost never what was wanted.

One array in, three different arrays out, and the input is unchanged every time. Note the second line: the result does not have to be the same type as the input. Strings in, numbers out.

Example 3.7.1: Mapping over objects

Because a callback can do anything, .map() over an array of objects is how records get reshaped.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

["Marisol","Dev"]
[true,false]
["Marisol: 92","Dev: 78"]
Concepts in Practice: map

1. What does [1, 2, 3].map((n) => n * n) produce?

  1. [1, 2, 3]
  2. [1, 4, 9]
  3. 14
Solution

b. [1, 4, 9]. Each element is squared and the results are collected into a new array of the same length.

Option c would be the sum of the squares, which .map() never does — it does not combine results, it collects them.

2. After const b = a.map((n) => n * 2);, what has happened to a?

  1. It now holds the doubled values.
  2. It is unchanged.
  3. It is empty.
Solution

b. Unchanged. .map() builds a new array and leaves the original alone — which is why the result has to be assigned to something.

Try It Now 3.7.1

Given const temps = [0, 100, 37];, use .map() to build an array of the same temperatures in Fahrenheit (c * 9 / 5 + 32), then print both arrays.

Editor
runs in your browser
▶ Press Run to see the output…
Solution
Editor
runs in your browser
▶ Press Run to see the output…

Output:

[32,212,98.6]
[0,100,37]

3.7.2 Taking Part of a List with .slice()

.slice(start, end) returns a copy of part of an array: from start up to but not including end.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

["b","c"]
["c","d","e"]
["a","b"]
["a","b","c","d","e"]

Three points, in order of how often they trip people up:

  1. The end index is excluded. slice(1, 3) gives positions 1 and 2, not 1, 2 and 3. The count of items you get is end - start.
  2. Leaving off end means "to the finish". slice(2) takes everything from position 2 onward.
  3. The original is untouched — the last line proves it.
Definition 3.7.2: .slice()

.slice(start, end) returns a new array containing the elements from index start up to but not including index end. With end omitted it runs to the end of the array. The original array is not changed.

Calling it with no arguments at all gives a full copy, which matters more than it looks:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[1,2,3]
[1,2,3,4]

That is the whole point of the methods in this section. push and pop from Section 3.3 change the array they are called on, which means anything else holding a reference to it sees the change. .map(), .slice() and .concat() never do — they hand back something new. When a function must not disturb its caller's data, these are how you keep that promise.

Section 3.6 explained why: an array is an object, so const copy = original; would give you a second name for the same array, and pushing would change both. .slice() makes an actual second array.

Example 3.7.2: Splitting a list in half

Halving a list is the first step of several classic algorithms, and it is .slice() twice.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[8,3,5]
[1,9,2]
true

Because slice(0, mid) excludes mid and slice(mid) includes it, the two halves fit together exactly with no item lost or repeated. You will meet this exact pair again in Chapter 11.

Try It Now 3.7.2

Given const week = ["Mon","Tue","Wed","Thu","Fri","Sat","Sun"];, print the first three days and the weekend. Then make a full copy, push something onto the copy, and show that week itself did not change.

Editor
runs in your browser
▶ Press Run to see the output…
Solution
Editor
runs in your browser
▶ Press Run to see the output…

Output:

["Mon","Tue","Wed"]
["Sat","Sun"]
8
7

The copy grew and the original did not, which is the whole reason .slice() with no arguments is worth knowing.

3.7.3 Joining Lists with .concat()

.concat() is the opposite of .slice(): it returns a new array with another array's items added on the end.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[1,2,3,4]
[1,2]
[3,4]

Both originals survive. .concat() also takes loose values and several arrays at once:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[1,2,3]
[1,2,3,4]
Definition 3.7.3: .concat()

.concat(...values) returns a new array made of the original's elements followed by the given values. Any argument that is an array has its elements added individually rather than as a nested array. Neither the original nor the arguments are changed.

Concepts in Practice: concat and push

1. What is the difference between a.push(b) and a.concat(b) when both a and b are arrays?

  1. Nothing.
  2. push changes a and adds b as one nested item; concat leaves a alone and returns a new flat array.
  3. concat changes a and push does not.
Solution

b. Two differences at once, which is why they are easy to confuse.

Editor
runs in your browser
▶ Press Run to see the output…

c is [1, 2, 3, 4]. a becomes [1, 2, [3, 4]] — changed in place, with b sitting inside it as a single nested element.

3.7.4 Spreading an Array into Arguments

Some functions take several separate arguments rather than one array. Math.max is the standard example:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

9
NaN

Handed three numbers it works. Handed one array it produces NaN, because an array is not a number and Math.max was never asking for a list.

The spread operator ... unpacks an array into separate arguments:

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

9

Math.max(...numbers) becomes Math.max(3, 9, 4). The three dots go at the call, not in the array.

Definition 3.7.4: Spread Operator

The spread operator ... expands an array's elements into separate items. In a function call it supplies them as individual arguments; inside a new array or object literal it copies the contents in.

You will meet this in Chapter 12 as union(...letters) — a function that combines any number of shapes, handed an array of five of them.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

a-b-c

Reading ... in a signature

The same three dots mean the mirror image when they appear in a function's definition — gather all the arguments into an array. You will read this in library documentation from Chapter 8 onward, in signatures like measureVolume(...geometries):

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

6
10

That is a rest parameter. total(...numbers) in a definition means "collect however many arguments into an array called numbers". total(...numbers) at a call site means the reverse. Same symbol, opposite direction — which side of the function it appears on tells you which.

Concepts in Practice: Spread

1. Why does Math.max([3, 9, 4]) give NaN?

  1. Math.max cannot handle three numbers.
  2. It received one argument that is an array, not a number.
  3. The array must be sorted first.
Solution

b. Math.max compares the arguments it is given. Given a single array, it tries to treat that array as a number and gets NaN. Math.max(...[3, 9, 4]) spreads it into three arguments and works.

Try It Now 3.7.3

Use spread with Math.min to find the lowest of const scores = [88, 72, 95, 64];, then write a function average(...nums) using a rest parameter and call it with three loose numbers.

Editor
runs in your browser
▶ Press Run to see the output…
Solution
Editor
runs in your browser
▶ Press Run to see the output…

Output:

64
6

3.7.5 Spreading into a New Array or Object

Spread also works inside a literal, where it copies the contents in.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

[1,2,3,4]
[0,1,2,99]
[1,2]

[...first, ...second] does the same job as first.concat(second). Both are common; spread reads better when you are mixing loose values in, as the second line does.

Copying an object with changes

The object form is the one you will reach for most, because it solves a problem that has no neat alternative: make a copy of an object with one or two values different.

Editor
runs in your browser
▶ Press Run to see the output…

What you should see:

{"width":10,"height":4,"color":"blue"}
{"width":10,"height":40,"color":"red"}
{"width":10,"height":4,"color":"red"}

Read { ...base, color: "blue" } as "everything in base, then color set to blue". Order matters: the later entry wins, so the override must come after the spread. Written the other way round, { color: "blue", ...base }, the spread would put the original red back.

The original base is untouched in both cases, which is exactly the guarantee Section 3.6 said you have to work for when objects are involved.

This is how the options objects of Section 3.5.6 get reused. You define one set of defaults, then spread it into each call with the one or two values that differ — { ...defaults, radius: 20 }. Chapter 12 does precisely this to recolour a shape without rebuilding it.

Concepts in Practice: Object spread

1. What is { ...base, size: 10 } when base is { size: 4, color: "red" }?

  1. { size: 4, color: "red" }
  2. { size: 10, color: "red" }
  3. An error, because size appears twice
Solution

b. { size: 10, color: "red" }. The spread copies everything in, then the later size: 10 overwrites the copied size: 4. Duplicate keys are not an error — the last one wins.

Try It Now 3.7.4

Given const settings = { theme: "dark", fontSize: 14, wrap: true };, build a copy with fontSize 18 without changing the original, then a copy with a new language property added.

Editor
runs in your browser
▶ Press Run to see the output…
Solution
Editor
runs in your browser
▶ Press Run to see the output…

Output:

{"theme":"dark","fontSize":18,"wrap":true}
{"theme":"dark","fontSize":14,"wrap":true,"language":"en"}
{"theme":"dark","fontSize":14,"wrap":true}

Overriding an existing property and adding a new one are the same operation — whether the key already existed is the only difference.

3.7.6 What Else Is Out There

Arrays have many more methods than the four this section teaches. Those four are here because they are the ones this book goes on to use; the rest are worth knowing exist so you recognize them in other people's code.

Chapter 11 implements sorting and searching by hand rather than calling .sort(), because writing them is the lesson there — the same reason Section 2.2 walked a list by hand before you ever saw .map().

Problem Set 3.7

3.7.1 What does [1, 2, 3].map((n) => n * n) produce, and why is the answer not a single number?

Solution

Step 1 — Apply the callback to each element: .map() calls (n) => n * n once per item, collecting each return value.

$$[1, 2, 3] \mapsto [1 \cdot 1,\; 2 \cdot 2,\; 3 \cdot 3] = [1, 4, 9]$$

Step 2 — Explain why it is not a single number: .map() does not combine results; it collects them. Each callback call returns one value, and the method gathers all of those values into a new array of the same length as the original. Combining elements into one value is the job of .reduce(), not .map().

Answer: [1, 4, 9] — an array, because .map() always returns a new array of the same length as the input, never a single combined number.

3.7.2 After const b = a.map((n) => n * 2);, what has happened to a?

Solution

Step 1 — Recall what .map() guarantees: .map() builds and returns a new array; it never modifies the array it was called on. That is why the result must be assigned to a variable (b) to be kept at all.

Step 2 — State the consequence for a: Since a is only read from (its elements are passed to the callback), nothing about a changes. If a was [3, 5], after the line it still holds [3, 5], while b holds [6, 10].

Answer: a is unchanged — .map() is non-mutating and returns a brand-new array.

3.7.3 What are the two differences between a.push(b) and a.concat(b) when both are arrays?

Solution

Step 1 — Difference in mutation: a.push(b) changes a in place — it modifies the array it is called on. a.concat(b) leaves both arrays alone and returns a new array instead.

Step 2 — Difference in nesting: push adds its argument as a single item, so if b is an array it ends up nested inside: a.push(b) on a = [1, 2], b = [3, 4] gives [1, 2, [3, 4]]. concat flattens array arguments one level, adding their elements individually: a.concat(b) gives [1, 2, 3, 4].

Editor
runs in your browser
▶ Press Run to see the output…

Output:

[1,2,3,4]
[1,2,[3,4]]

Answer: push mutates a and nests b as one element; concat leaves everything unchanged and returns a new flat array with b's elements added individually.

3.7.4 Why does Math.max([3, 9, 4]) give NaN, and what one change fixes it?

Solution

Step 1 — Diagnose the failure: Math.max expects separate numeric arguments, e.g. Math.max(3, 9, 4). Called as Math.max([3, 9, 4]), it receives exactly one argument that happens to be an array. It tries to convert that array to a number to compare it, conversion fails, and the result is NaN.

Step 2 — Apply the fix: The spread operator unpacks the array into individual arguments:

Editor
runs in your browser
▶ Press Run to see the output…

Output:

9

Now the call is equivalent to Math.max(3, 9, 4), which correctly returns 9.

Answer: It fails because Math.max got one array argument rather than numbers; spreading it — Math.max(...numbers) — fixes the call.

3.7.5 What is { ...base, size: 10 } when base is { size: 4, color: "red" }?

Solution

Step 1 — Read the literal left to right: { ...base, size: 10 } first copies every property of base into the new object (size: 4, color: "red"), then processes the later entry size: 10.

Step 2 — Apply the "later entry wins" rule: When two entries share a key, the one written last overwrites the earlier one. Duplicate keys are not an error in JavaScript object literals. So the copied size: 4 is replaced by size: 10, while color survives untouched.

Answer: { size: 10, color: "red" } — a new object; base itself is unchanged.

3.7.6 Use .map() to turn [0, 100, 37] into Fahrenheit with c * 9 / 5 + 32.

Solution

Step 1 — Write the mapping: Supply the given formula as the callback so .map() applies it to each Celsius temperature:

Editor
runs in your browser
▶ Press Run to see the output…

Step 2 — Check each value: \(0 \cdot 9/5 + 32 = 32\), \(100 \cdot 9/5 + 32 = 212\), \(37 \cdot 9/5 + 32 = 66.6 + 32 = 98.6\).

Output:

[32,212,98.6]

Answer: [32, 212, 98.6] — produced by celsius.map((c) => c * 9 / 5 + 32).

3.7.7 Given a seven-day array, print the first three days and the last two using .slice().

Solution

Step 1 — Take the first three days: slice(0, 3) copies indices 0 through 2 (the end index is excluded), giving "Mon", "Tue", "Wed".

Step 2 — Take the last two days: With seven days, the weekend starts at index 5. Omitting the end index means "run to the finish", so slice(5) gives "Sat" and "Sun".

Editor
runs in your browser
▶ Press Run to see the output…

Output:

["Mon","Tue","Wed"]
["Sat","Sun"]

Answer: week.slice(0, 3) prints ["Mon","Tue","Wed"] and week.slice(5) prints ["Sat","Sun"].

3.7.8 Explain why .slice() with no arguments is useful, referring to Section 3.6.

Solution

Step 1 — Recall the reference problem from Section 3.6: Arrays are objects, so assignment copies a reference, not the contents. Writing const copy = week; would give two names for the same array, and any change made through one name would be visible through the other.

Step 2 — Show how .slice() solves it: week.slice() with no arguments copies from index 0 to the end — i.e. the whole array — but into a genuinely new array. Pushing onto the copy grows the copy's length while week.length stays at 7:

Editor
runs in your browser
▶ Press Run to see the output…

Output:

8
7

Answer: .slice() with no arguments makes a true independent copy, avoiding the shared-reference aliasing problem from Section 3.6 — changes to the copy do not affect the original.

3.7.9 Given const list = [8, 3, 5, 1, 9, 2];, split it into two halves with .slice() so that no item is lost or repeated. Prove it with a comparison of lengths.

Solution

Step 1 — Find the midpoint: The list has 6 items, so Math.floor(list.length / 2) is Math.floor(3) = 3.

Step 2 — Cut with complementary slices: slice(0, mid) takes indices 0–2 (excluding 3), and slice(mid) takes everything from index 3 onward. Because the first slice excludes mid and the second includes it, the halves meet exactly with no gap or overlap.

Editor
runs in your browser
▶ Press Run to see the output…

Output:

[8,3,5]
[1,9,2]
true

Answer: left = [8, 3, 5], right = [1, 9, 2], and list.length === left.length + right.length prints true, proving no item was lost or repeated.

3.7.10 Write average(...nums) using a rest parameter, and call it with four numbers.

Solution

Step 1 — Write the function with a rest parameter: In the definition, ...nums collects however many arguments are passed into an array called nums, which we can then loop over:

Editor
runs in your browser
▶ Press Run to see the output…

Step 2 — Call it with four numbers: The four loose arguments are gathered into nums = [2, 4, 6, 8]; the loop sums them to 20, and dividing by nums.length gives \(20 / 4 = 5\).

Editor
runs in your browser
▶ Press Run to see the output…

Output:

5

Answer: average(...nums) uses a rest parameter to gather all arguments into an array; average(2, 4, 6, 8) returns 5.

3.7.11 Explain what ... means in total(...numbers) when it appears in a definition, and what it means in the same text at a call site.

Solution

Step 1 — In a definition (rest parameter): In function total(...numbers), the three dots mean gather: collect all the arguments actually passed into a single array named numbers. The function can then treat numbers like any other array, looping over it or indexing into it, regardless of how many arguments were supplied.

Step 2 — At a call site (spread): In total(...values), the same three dots mean the opposite — spread: take the array values and expand it into separate individual arguments, as if each element had been written out by hand.

Answer: Same symbol, opposite directions: in a definition ...numbers collects arguments into an array (rest parameter); at a call site ...array unpacks an array into separate arguments (spread). Which side of the function it appears on tells you which.

3.7.12 Given const settings = { theme: "dark", fontSize: 14 };, build a copy with fontSize 18 and a separate copy with a language property added.

Solution

Step 1 — Copy with fontSize overridden: Spread all of settings into a new object, then restate fontSize last so the later entry wins:

Editor
runs in your browser
▶ Press Run to see the output…

Step 2 — Copy with a new property added: Adding a key that did not exist before works identically — spread everything in, then add the new entry:

Editor
runs in your browser
▶ Press Run to see the output…

Output:

{"theme":"dark","fontSize":18}
{"theme":"dark","fontSize":14,"language":"en"}
{"theme":"dark","fontSize":14}

Answer: bigger = { theme: "dark", fontSize: 18 } and localized = { theme: "dark", fontSize: 14, language: "en" }, both built with object spread; the original settings is untouched.

3.7.13 Why does { color: "blue", ...base } fail to override the colour when base already has one?

Solution

Step 1 — Trace the order of operations: Object literals are processed top to bottom. { color: "blue", ...base } first sets color: "blue", but then the spread copies in all of base's properties — including its own color — afterwards.

Step 2 — Apply the "later entry wins" rule: Since the spread comes last, base's colour overwrites the blue one. For the override to stick, it must be written after the spread: { ...base, color: "blue" }.

Answer: It fails because the spread comes after the override, so base's existing color is copied in last and wins; the fix is to put the override after the spread.

3.7.14 Name one array method that changes the array it is called on and one that does not, and say how you would decide which you need.

Solution

Step 1 — Name one mutating and one non-mutating method: .push() changes the array it is called on, adding an element in place. .map() does not — it returns a brand-new array and leaves the original exactly as it was. (Other valid pairs: .sort() vs .slice(), .reverse() vs .concat().)

Step 2 — How to decide: Ask whether other parts of the program may still hold a reference to this array. If the caller's data must stay intact — or you simply want a transformed/copied result to assign elsewhere — use a non-mutating method (.map(), .slice(), .concat(), object/array spread). Use a mutating method only when you deliberately intend to modify the array in place and every holder of a reference should see the change.

Answer: .push() mutates; .map() does not. Choose based on whether the original data must be preserved for other holders of the reference — preserve it with non-mutating methods, change it in place only intentionally.

Key Terms

.map() -- Builds a new array by calling a callback once per element and collecting the results. Same length, original unchanged.

.slice(start, end) -- Returns a copy of part of an array, from start up to but not including end. Original unchanged.

.concat() -- Returns a new array of the original followed by the given values, flattening array arguments one level.

Spread operator (...) -- Expands an array into separate arguments at a call, or copies contents into a new array or object literal.

Rest parameter -- ...name in a function definition, collecting however many arguments were passed into an array.

Non-mutating method -- One that returns something new and leaves the original alone, like .map(), .slice() and .concat().

Mutating method -- One that changes the array it is called on, like .push(), .sort() and .reverse().

Object spread with override -- { ...original, key: newValue }, a copy with selected values replaced. The later entry wins, so the override must come last.