Two kids share one toy. They don't each have one, it is the same toy. If one kid paints it blue, the other kid's toy is blue too. A copy is when each kid gets their own toy.
b = a does not make a copy
When you put an array or object into a variable, the variable holds a reference to it, a bit like an address that says where the array lives. Writing const b = a copies the address, not the array. Both names now point to the same array.
const a = [1, 2];
const b = a;
b.push(3);
console.log(a); // prints: [ 1, 2, 3 ]
console.log(a === b); // prints: true
You pushed into b, and a changed too, because there was only ever one array. Objects behave the same way.
const cat = { name: "Tom" };
const sameCat = cat;
sameCat.name = "Kit";
console.log(cat.name); // prints: Kit
JavaScript shares by default for speed. Copying a big array every time you passed it around would be slow, so it shares the same one unless you ask for a copy.
Make a real copy with spread
To get your own toy, use spread. [...a] makes a new array with the same items, and { ...obj } makes a new object with the same properties.
const a = [1, 2];
const b = [...a];
b.push(3);
console.log(a); // prints: [ 1, 2 ]
console.log(b); // prints: [ 1, 2, 3 ]
Now b is its own array. Change it, and the original stays the same.
One thing to know. Spread copies one level. If the array or object holds other arrays or objects, those inner ones are still shared.
const kid = { name: "Ana", toys: ["car"] };
const copy = { ...kid };
copy.toys.push("ball");
console.log(kid.toys); // prints: [ 'car', 'ball' ]
For a full copy all the way down, use structuredClone, which is built into modern browsers and Node.
const kid = { name: "Ana", toys: ["car"] };
const deep = structuredClone(kid);
deep.toys.push("ball");
console.log(kid.toys); // prints: [ 'car' ]The twist: numbers and strings are safe
Numbers and strings don't have this problem. They can't be changed in place. There is no push for a number. So y = 10 just gives y a new value, and x is left alone.
let x = 5;
let y = x;
y = 10;
console.log(x); // prints: 5
The same is true for arrays when you assign a new one instead of changing the old one.
let a = [1, 2];
let b = a;
b = [9];
console.log(a); // prints: [ 1, 2 ]
So the surprise only happens when you change an array or object in place, with things like push, pop, sort, or setting a property.
When to copy
Copy before you change something that came from somewhere else, such as an array passed into your function. The caller probably doesn't expect their data to change.
Watch out for methods that change the array they are called on. sort and reverse both work in place. If you want a sorted version and want to keep the original, sort a copy with [...scores].sort(), or use scores.toSorted() in newer environments.
References also explain why comparing two arrays with === can surprise you. It checks whether both sides are the same array, not whether they hold the same items.
const a = [1, 2];
const b = [...a];
console.log(a === b); // prints: false
console.log(a === a); // prints: true
The copy has the same items, but it is a different array, so === says false.
Rule of thumb
If you want to change an array or object without changing the original, copy it first.
Many beginners think const stops an array from changing. const only stops you from pointing the name at a different value, so push on a const array still works and still affects every name that shares it.
Check yourself
const a = [1]; const b = a; b.push(2);, what is a?b = a copies the reference, so both names point to one array. Pushing through b changes that one array, which a also sees.