What you'll be able to do
By the end of this page you should be able to create an array, read and change items by index, use length, add/remove with push / pop, and explain why list2 = list1 does not copy the list — both names point at the same array.
Previous post: errors — try, catch, throw.
Who this is for
- People who store several values in
a,b,cand wish for one list - Beginners who write
list2 = list1and wonder why both lists change afterpush - Anyone who has seen
undefinedwhen reading past the end of an array
You can skip this if you already use indexes safely, know push mutates, and copy with slice or spread when you need a second list. Come back when a “copy” still shares data.
An array is an ordered list
An array holds values in order. The first item is at index 0, the second at 1, and so on.
const tags = ["javascript", "tutorials", "developers"];
console.log(tags[0]); // "javascript"
console.log(tags.length); // 3
Scenario A — a tag list on a Learn post. One array, several strings, easy to loop later.
Scenario B — scores for one quiz. Numbers in order: [8, 10, 7]. Same idea: position matters.
Prefer the literal [...] when you start. You rarely need new Array(3) as a beginner — that form is easy to misuse.
Read and write by index
const colors = ["red", "green", "blue"];
console.log(colors[1]); // "green"
colors[1] = "teal";
console.log(colors); // ["red", "teal", "blue"]
Scenario A — fix a typo in place. Change colors[1] and the array updates.
Scenario B — reading past the end. colors[99] is undefined — not a crash. Check index < colors.length when the index comes from user input or a loop.
length is the count of items. The last index is length - 1 when the array is not empty.
push and pop change the same array
push adds to the end. pop removes from the end. Both mutate the array (they change it in place).
const queue = ["A", "B"];
queue.push("C");
console.log(queue); // ["A", "B", "C"]
const last = queue.pop();
console.log(last); // "C"
console.log(queue); // ["A", "B"]
Scenario A — a to-do list in memory. push a new task; pop if you only care about the last one (stack style).
Scenario B — logging what changed. After push, every variable that points at queue sees the new length. That is useful — and dangerous when you thought you had a private copy.
We covered pure vs impure earlier: mutating an array you share is a side effect on shared state.
The big surprise: list2 = list1 shares one array
const list1 = ["x", "y"];
const list2 = list1; // same array, two names
list2.push("z");
console.log(list1); // ["x", "y", "z"] — changed too
Scenario A — “backup” that is not a backup. You assign backup = cart, then cart.push(item), and backup grows. Both names were the same cart.
Scenario B — a function that receives an array. If the function pushes, the caller’s array changes unless you copied first.
Rule: assignment copies the reference (the pointer), not a new list of values.
How to get a real second list (shallow copy)
const list1 = ["x", "y"];
const list2 = list1.slice(); // new array, same item values
// or: const list2 = [...list1];
list2.push("z");
console.log(list1); // ["x", "y"]
console.log(list2); // ["x", "y", "z"]
Scenario A — experiment without breaking the original. Copy, then mutate the copy.
Scenario B — nested objects inside the array. slice / spread copy the outer list only. Inner objects are still shared. That deeper story comes later with objects and shallow vs deep copy — for now, know that string/number items are safe to treat as independent after a shallow copy.
const arrays can still mutate
const nums = [1, 2];
nums.push(3); // allowed
// nums = [9]; // would throw — rebinding the name
Scenario A — const means the binding stays. The variable always points at that array object.
Scenario B — people expect const to freeze contents. It does not. Use const for the name; use copy-or-don’t-mutate habits for the contents.
A tiny practice file
Save as arrays.html. Add tags; see shared vs copied.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Arrays</title>
</head>
<body>
<input id="tag" placeholder="New tag" />
<button id="addShared">Push on shared copy</button>
<button id="addCopied">Push on real copy</button>
<pre id="out"></pre>
<script>
const original = ["javascript"];
const shared = original;
const copied = original.slice();
function show() {
document.getElementById("out").textContent =
"original: " + JSON.stringify(original) + "\n" +
"shared: " + JSON.stringify(shared) + "\n" +
"copied: " + JSON.stringify(copied);
}
document.getElementById("addShared").addEventListener("click", function () {
const t = document.getElementById("tag").value.trim() || "item";
shared.push(t);
show();
});
document.getElementById("addCopied").addEventListener("click", function () {
const t = document.getElementById("tag").value.trim() || "item";
copied.push(t);
show();
});
show();
</script>
</body>
</html>
Click “shared” a few times — original grows too. Click “copied” — only copied grows.
Mistakes I see a lot
1. list2 = list1 as a copy. It is not. Use slice or spread.
2. Off-by-one on the last item. Last index is length - 1, not length.
3. Assuming missing indexes throw. Out-of-range reads give undefined.
4. Thinking const freezes the array. Contents can still change.
5. Using new Array(5) for five empty slots. Prefer [1, 2, 3] literals until you know why sparse arrays exist.
6. Mutating a shared array inside a helper and returning it. Callers keep a surprise reference — same theme as impure functions.
What to try before the next post
- Build
["a","b","c"], log index0andlength. pushone item;popit back.- Assign
b = a,pushonb, loga. - Repeat with
b = a.slice()and compare. - Build the tiny HTML file.
Next in this series: forEach and map — when to transform into a new list vs when to only run a side effect.
Try this next outside the series
Arrays get easier when you see them render UI lists and move through real transforms.
- React hooks patterns — see arrays, mapping, and derived state inside everyday React code
- JSON Formatter — inspect array payloads from APIs without losing the shape