← Fundamentals
Guided lesson 6 questions · 6 languages

Slices, views, and shared storage

Whether a selected range copies its elements or shares them, and what happens when either side changes.

Selecting a range gives either a copy, with storage of its own, or a view, which shares the storage of the collection it came from. An ordinary array’s slice always copies. A typed array’s subarray is a view: a change through either one shows in both, until the source is replaced.

Before you begin

Why this matters

Code often works on part of a collection: the first ten results, one week of a log, one row of a picture. If you think you hold a copy and you hold a view, a change you make in one place appears in another. If you think you hold a view and you hold a copy, a change you expected to see never arrives.

What this helps you reason about

  • Predict whether a change to the original shows in a selected part, and the other way round.
  • Tell slice, which copies, from a typed array’s subarray, which shares.
  • Spot a view that still reads old storage after its array was replaced.

A stamp collector keeps an album and counts the stamps on each page: [8, 5, 12, 3]. This lesson selects a range of pages, such as pages 1 and 2, and asks whether the selected part is a copy of those numbers or a second way to reach them, and what happens when either side changes.

Already know this? The short version
  • array.slice(start, end) copies the elements from start up to, not including, end; after it, the copy and the original never affect each other.
  • slice copies one level: when the elements are objects, both arrays hold references to the same objects.
  • A typed array’s subarray(start, end) is a view over the same buffer, and writes through either one show in both; its slice copies.
  • A typed array cannot grow. Growing means a new, larger one, and a view made earlier keeps reading the old buffer.
  • Copy when either side will change and the other must not see it, or to keep a small part of a large buffer; share when later changes should show.

Go to the final check· Go to the practice tickets

Words in this lesson (4)
range
The part of a sequence from a start index up to an end index. The start is included and the end is not, so the range from 1 to 3 holds indexes 1 and 2, and its length is end − start.
typed array
An array of numbers of one fixed kind, such as Uint8Array, whose elements are whole numbers from 0 to 255. Its length never changes, and its elements are stored in a block of memory called a buffer.
view
A value that reaches a range of another collection’s elements without copying them. A change made through the view shows in the collection, and a change made in the collection shows through the view.
reference
A value that leads to an object. A variable whose value is an object holds a reference, not a copy of the object, so copying the variable gives a second way to reach the same object.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / Selecting a range

What does selecting a range give you?

A range is part of a sequence: from a start index up to an end index. The start is included and the end is not. The range from 1 to 3 holds indexes 1 and 2, and its length is 3 − 1 = 2. Most languages write ranges this way, so that the length is always end − start.

album.slice(1, 3) selects that range of an array. This lesson asks what slice gives back: a new array with its own 5 and 12, or a second way to reach the elements of album. The answer decides what happens when either side changes later. The first experiment changes album after the slice.

main.ts
const album = [8, 5, 12, 3];
const part = album.slice(1, 3); // indexes 1 and 2
console.log(part); // [ 5, 12 ]
console.log(part.length); // 2, which is 3 - 1

A range runs from its start index up to, but not including, its end index; its length is end − start.

Coming from Python: album.slice(1, 3) is Python’s album[1:3], and it copies in the same way.

Precisely: indexes outside the array

slice also accepts a negative index, counted from the end: album.slice(-2) selects the last two elements. An end past the length stops at the length, without an error.

Experiment

Select a range, then change the array

slice made a new array and copied 5 and 12 into it. album and part are two arrays with separate elements, so a write to one never reaches the other.

Why it matters An array’s slice is a copy: to see later changes, read the original array again.

Predict both printed arrays. Then change album[1] = 6; to part[0] = 6; and predict again.

const album = [8, 5, 12, 3];
const part = album.slice(1, 3);
album[1] = 6;
console.log(part, album);
What to look for

The stepper stops before each line runs, and it stops twice on the console.log line. After slice, part shows [5, 12]. After album[1] = 6, album shows [8, 6, 12, 3] while part keeps [5, 12], and the run prints [ 5, 12 ] [ 8, 6, 12, 3 ]. After the edit, part shows [6, 12] while album keeps [8, 5, 12, 3].

Expected output

[ 5, 12 ] [ 8, 6, 12, 3 ]

02 / Copies

When is the selected part a copy?

slice returns a new array and copies each selected element into it. After that, the two arrays have nothing in common: album[1] = 6 changes album only, and part[0] = 6 changes part only.

A copy costs one step per selected element, and new memory for each of them. That is the price of a part that nothing else can change.

main.ts
const album = [8, 5, 12, 3];
const part = album.slice(1, 3);
album[1] = 6;
console.log(part); // [ 5, 12 ]: the copy keeps the old value

slice copies: after it, a change to either array does not reach the other.

Coming from Go: album.slice(1, 3) is not Go’s album[1:3]: it copies, so nothing written through the part reaches the array.

Precisely: one level only

slice copies the values stored in the array. When the elements are objects, those values are references, so both arrays lead to the same objects: changing a property through part[0] changes what album[1] leads to as well. A later lesson on shallow and deep copying follows each level. [...album] and Array.from(album) copy in the same way.

03 / Views

When does the selected part share storage?

A view reaches a range of another collection’s elements without copying them. Plain arrays have no views, but typed arrays do. A typed array, such as Uint8Array, is an array of numbers of one fixed kind; a Uint8Array holds whole numbers from 0 to 255, and its length never changes. Its elements are stored in a block of memory called a buffer.

album.subarray(1, 3) makes a new Uint8Array that uses the same buffer, starting at index 1 of album. part[0] and album[1] are the same place in memory, so a change through either one shows through both. Making a view takes one step however long the range is, because nothing is copied. A typed array also has slice, which copies, like an array’s slice.

main.ts
const album = new Uint8Array([8, 5, 12, 3]);
const part = album.subarray(1, 3); // a view of indexes 1 and 2
part[0] = 6;
console.log(album[1]); // 6: the same place in memory

subarray shares the buffer: a write through the view shows in the source, and the other way round.

Coming from Node.js: buf.slice() on a Node.js Buffer is a view, like subarray and unlike every other slice in this lesson; Node.js deprecates it for that reason.

Precisely: the buffer

The buffer is an ArrayBuffer: album.buffer === part.buffer is true, and part.byteOffset is 1, where the view starts in the buffer. Code that wants a live part of a plain array keeps the start and end indexes and reads album[i] when it needs a value.

Experiment

Write through a subarray

subarray made a new Uint8Array over album’s buffer, starting at index 1, so part[0] is album[1]. slice copies the two elements into a new buffer.

Why it matters On a typed array, subarray shares and slice copies; choose by whether a change should reach the original.

Predict the two printed numbers. Then change subarray to slice and predict again.

const album = new Uint8Array([8, 5, 12, 3]);
const part = album.subarray(1, 3);
part[0] = 6;
console.log(album[1], part.length);
What to look for

A typed array shows as its indexes and values: album shows {0: 8, 1: 5, 2: 12, 3: 3}, and part shows {0: 5, 1: 12}. After part[0] = 6, album shows 1: 6 as well, and the run prints 6 2. After the edit, part changes to {0: 6, 1: 12} while album keeps 1: 5, and the run prints 5 2.

Expected output

6 2

04 / Source grows

What happens to a view when its source grows?

A typed array cannot grow. To add a page, a program makes a larger typed array, copies the old elements into it with bigger.set(album), which writes album’s elements into bigger from index 0, and then uses the larger one. A view made before that still uses the old buffer. It does not follow the variable album to the new array, because a view refers to a buffer, not to a variable.

From then on, changes to album do not show in the view, and the view shows old values without any error. Make the view again from the new array, after the move.

main.ts
let album = new Uint8Array([8, 5, 12, 3]);
const part = album.subarray(1, 3);
const bigger = new Uint8Array(6); // six places, all 0
bigger.set(album); // copies 8, 5, 12, 3 to the front
album = bigger;
album[1] = 6;
console.log(part[0]); // 5: part still reads the old buffer

A view keeps the storage it was made from; after the source is replaced, make the view again.

Precisely: detached buffers

Since ES2024, buffer.transfer() moves an ArrayBuffer’s contents to a new one and detaches the old buffer; every view over a detached buffer then has length 0. A resizable ArrayBuffer can also grow in place. This lesson uses neither.

Experiment

Keep a view, then replace the array

part is a view of the old buffer. album = bigger makes the variable lead to a new array; it does not move part. Only a view made from the new array shares its buffer.

Why it matters After a typed array is replaced by a larger one, make its views again.

Predict the two printed numbers. Then move the line const part = album.subarray(1, 3); to just after album = bigger; and predict again.

let album = new Uint8Array([8, 5, 12, 3]);
const part = album.subarray(1, 3);
const bigger = new Uint8Array(6);
bigger.set(album);
album = bigger;
album[1] = 6;
console.log(part[0], album[1]);
What to look for

bigger starts as six zeros; after bigger.set(album) it shows {0: 8, 1: 5, 2: 12, 3: 3, 4: 0, 5: 0}. After album = bigger, album shows the six places while part keeps {0: 5, 1: 12}. After album[1] = 6, album and bigger show 1: 6, part does not change, and the run prints 5 6. After the edit, part is made from the new array, and the run prints 6 6.

Expected output

5 6

05 / Copy or view

When should you copy, and when should you share?

Choose a copy when either side will change and the other must not see it: a part you hand to code that may change it, or a record of the values as they were. Choose a view when you want to see later changes, or when copying a large range costs too much, and the source is not replaced while the view is in use.

A view also keeps its whole buffer in memory. A view of 4 bytes from a buffer of 10 million bytes keeps all 10 million reachable for as long as the view is. To keep a small part of a large buffer, copy the part.

main.ts
const scan = new Uint8Array(10000000); // 10 million bytes
const header = scan.slice(0, 4); // a copy: scan can be reclaimed
const live = scan.subarray(0, 4); // a view: keeps all of scan

Copy to keep a part separate or small; share to see changes while the source stays in place.

Precisely: when memory is reclaimed

Reachability and garbage collection explains when unreachable memory is reclaimed.

06 / Try it

Can you predict a fresh example?

Decide which part is a copy and which is a view, then follow each change. The next lesson, Stack and heap, shows where the storage that copies and views use is kept.

main.ts
const album = new Uint8Array([8, 5, 12, 3]);
const kept = album.slice(0, 2);
const shown = album.subarray(2, 4);
album[0] = 9;
shown[0] = 1;
console.log(kept[0], album[2], shown[1]);

What is printed?

Reveal the reasoning

8 1 3. kept is a copy, so album[0] = 9 does not reach it: kept[0] is still 8. shown is a view of indexes 2 and 3, so shown[0] = 1 writes album[2]. shown[1] is album[3], which nothing changed: 3.

Practice tickets

  1. SLICE-101 · Previewing a dimmed section also dims the saved lighting scene → Free
  2. SLICE-102 · Score corrections never change the evening total on the club screen → Sign in · Pro
  3. SLICE-103 · Marked rows show old stitch counts, or the wrong rows, after a cuff and a size change → Sign in · Pro

Keep exploring

Stack and heap →

Primary references