Viscosity
Review
Part I · JavaScript for engine builders · Day 1

4. References: two names, one object

A variable never holds an object. It holds an arrow to one. Once you see the arrows, half of JavaScript’s surprises disappear, and the hardest problem in this book comes into view.

About 14 minutes, including the exercises and quiz
By the end you will be able to
  • Predict what happens when two variables refer to the same object and one of them changes it.
  • Explain why === on objects compares identity, not contents.
  • Explain how a function can change an object its caller owns.
  • Make a shallow copy with spread syntax, and say what it does not copy.

A puzzle

Predict what each of these prints before you read on:

let m = 1;
let n = m;
n = 99;
console.log(m);

const a = { x: 1 };
const b = a;
b.x = 99;
console.log(a.x);

The first prints 1. Changing n didn’t touch m. The second prints 99. Changing b did change a. Same-looking code, opposite results. Why?

Intuition

A number in a variable is like a number written on a sticky note. Copy the note and you have two separate notes. An object is more like a house, and the variable holds only its address. const b = a copies the address onto a second card. Now two cards lead to the same house, and if you repaint the house using one card, the other card leads to a repainted house too.

The arrows

Step through it. On the left are the variables; on the right, the objects living in memory.

Step throughTwo names, one object
const a = { x: 1 };
const b = a;
b.x = 99;
console.log(a.x);
const c = { x: 99 };
console.log(a === b);
console.log(a === c);
Variables on the left hold arrows to objects on the rightVARIABLESOBJECTS IN MEMORYa{ x: 1 }
{ x: 1 } creates an object somewhere in memory. The variable a doesn’t hold the object itself: it holds an arrow (a reference) to it.
1/7
Definition

A variable that holds an object holds a reference to it: an arrow pointing at the object, not a copy of it. When two references point at the same object, they are aliases. Changing an object in place (like b.x = 99) is a mutation, and every alias sees it. For objects, === compares identity: “are these the same object?”, not “do they have the same contents?”.

This also solves a mystery from chapter 2: how can you push onto a const array? const freezes the arrow: the name can never point at a different array. It says nothing about the array the arrow points at, and that array can still be changed.

Arrays are objects too

Everything above holds for arrays, because arrays are objects:

const list = [1, 2, 3];
const same = list;
same.push(4);

console.log(list);         // [ 1, 2, 3, 4 ]
console.log(list === same); // true
console.log([1] === [1]);   // false

Two separately written [1]s are two different arrays, so === says false even though they look identical.

Try itArrows in action
const a = { x: 1 };
const b = a;
b.x = 99;
console.log(a.x);

const c = { x: 99 };
console.log(a === b, a === c);

const list = [1, 2];
const same = list;
same.push(3);
console.log(list);

Functions get arrows too

When you pass an object to a function, the parameter receives a copy of the arrow. Function and caller now share one object:

function birthday(person) {
  person.age = person.age + 1;
}

const ada = { name: 'Ada', age: 36 };
birthday(ada);
console.log(ada.age); // 37

That’s why step(particles) in the last chapter could move the caller’s particles without returning anything. But notice what a function can’t do: point the caller’s variable somewhere else.

function replace(person) {
  person = { name: 'Bob', age: 1 };
}

replace(ada);
console.log(ada.name); // Ada

person = ... re-aims only the function’s own local copy of the arrow. The caller’s ada arrow never moved.

Making a real copy

Sometimes you want a separate object. Spread syntax, {...obj}, builds a brand-new object and copies the properties into it:

const p = { x: 1, y: 2 };
const q = { ...p, x: 50 };

console.log(p.x);    // 1
console.log(q.x);    // 50
console.log(p === q); // false

Properties listed after the spread override the copied ones, so { ...p, x: 50 } means “a copy of p, but with x set to 50”.

Pitfall

Spread is shallow. It copies the properties of the outer object, but if a property holds an object, only the arrow is copied. So const c = { ...a } where a.pos is an object leaves c.pos === a.pos, and mutating c.pos.x still changes a.pos.x.

Why this is Viscosity’s hardest problem

Remember the particle loop. Suppose Viscosity decides your particles would be faster as a struct of arrays: one array of all the x values, one of all the y values. Now look at this ordinary code:

const first = particles[0];
first.x = 100;
console.log(particles[0].x); // must be 100

first and particles[0] are aliases. They’re the same object, and the program relies on that. If Viscosity had quietly moved x into a separate array, first.x = 100 would have to update that array too. Otherwise the program prints the wrong answer, and an optimizer that changes answers is worse than no optimizer at all.

So before it can change how data is laid out, Viscosity has to answer: where can arrows to these objects end up? Who else might be looking at them? Compilers call this alias analysis and escape analysis, and they’re what make automatic layout changes safe. Research systems that switch JavaScript arrays to columns automatically face exactly this requirement[3]. You’ll build these analyses yourself in Parts VI and VII. For now, you can see the problem, and that’s the first step to solving it.

Your turn

ExerciseChange x without changing the original

Write withX(p, x) that returns a new object just like p but with its x property set to x. The original p must not change.

function withX(p, x) {
  // your code here
}

const p = { x: 1, y: 2 };
const q = withX(p, 9);
console.log(p, q);
Extra practiceFind this exact object

Write indexOfSame(xs, target) that returns the index of the slot holding the very same object as target (identity, not equal contents), or -1 if there is none.

function indexOfSame(xs, target) {
  // your code here
}

const a = { id: 1 };
const xs = [{ id: 1 }, a];
console.log(indexOfSame(xs, a));

What you learned

  • A variable holding an object holds a reference: an arrow, not the object.
  • Copying the variable copies the arrow. Two arrows to one object are aliases, and a mutation through either is seen by both.
  • === on objects compares identity: same object, not same contents.
  • Functions receive arrows, so they can mutate the caller’s objects, but they can’t re-aim the caller’s variables.
  • { ...obj } makes a new, shallow copy.
  • Aliasing is why changing data layout automatically is hard. It is the problem at the core of Viscosity.

That’s the end of Day 1. Tomorrow: functions as values, and closures.

Check your understanding

0/6
  1. 1.

    What does this print?

    const a = { n: 1 };
    const b = a;
    b.n = 2;
    console.log(a.n);
  2. 2.

    What is [1, 2] === [1, 2]?

  3. 3.

    What does this print?

    function f(o) {
      o = { v: 2 };
    }
    const x = { v: 1 };
    f(x);
    console.log(x.v);
  4. 4.

    const c = { ...a }; and a.pos is an object. Which is true?

  5. 5.

    Why is aliasing a problem for an optimizer that changes an array of objects into a struct of arrays?

  6. 6.

    Which line creates a new object rather than another alias of p?

References

  1. [1] MDN contributors. Working with objects (JavaScript Guide). MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_objects (checked 2026-10-31)
  2. [2] MDN contributors. Equality comparisons and sameness. MDN Web Docs. developer.mozilla.org/en-US/docs/Web/JavaScript/Equality_comparisons_and_sameness (checked 2026-10-31)
  3. [3] Lukas Makor, Sebastian Kloibhofer, David Leopoldseder, Daniele Bonetta, Lukas Stadler, Hanspeter Mössenböck (2022). Automatic Array Transformation to Columnar Storage at Run Time. Proceedings of the 19th International Conference on Managed Programming Languages and Runtimes (MPLR ’22), ACM, pp. 16–28. doi:10.1145/3546918.3546919 (checked 2026-10-31)