Primitive vs. Reference Types in JavaScript

Hero image for Primitive vs. Reference Types in JavaScript. Image by Mylène Haudebourg.
Hero image for 'Primitive vs. Reference Types in JavaScript.' Image by Mylène Haudebourg.

This Article is Over Ten Years Old...

Things can and do move very quickly in tech, which means that tech-related articles go out of date almost as soon as they have been written and published. If you are looking for up-to-date technical advice or opinion, it is unlikely that you will find it on this page.

You may find that my recent articles are more relevant, and you are always welcome to drop me a line if you have a specific technical problem you are trying to solve.

Primitive values and reference types are one of those JavaScript fundamentals that quietly sit underneath a huge number of beginner bugs. You do not always hear the terminology first. More often, you meet the consequences.

Two arrays that look identical are somehow not equal. A copied object changes when another part of the code updates it. A string behaves nicely and independently whilst an object seems to spread changes further than expected.

All of that becomes easier to understand once you know which values are primitive and which are referencelike structures.


Primitive Values are the Simpler Category

In everyday JavaScript, primitive values are the small, basic values that are not objects.

The main primitives are:

  • string
  • number
  • boolean
  • null
  • undefined
  • symbol
  • bigint

For most frontend work, you spend the most time with the first five.

These values behave simply when assigned and compared. If you assign a primitive to another variable, you get a new independent variable holding that value.

let firstName = 'Sophie';let copiedName = firstName;copiedName = 'Ellie';console.log(firstName);  // Sophieconsole.log(copiedName);  // Ellie

Changing copiedName does not alter firstName.


Objects, Arrays, and Functions are Not Primitives

Arrays, plain objects, dates, and functions all belong to the object side of JavaScript.

That means they behave differently when assigned.

const originalUser = {  name: 'Sophie',};const copiedUser = originalUser;copiedUser.name = 'Ellie';console.log(originalUser.name);  // Ellie

That surprises beginners because copiedUser sounds as if it should be a fresh copy. It is not. Both variables point to the same underlying object.

That is where the topic starts to matter. Primitive assignment feels like copying the value. Object assignment feels like sharing access to the same thing.


Equality Behaves Differently Too

One of the cleanest ways to see the distinction is with equality.

Primitives compare by value:

console.log(5 === 5);  // trueconsole.log('hello' === 'hello');  // true

Objects compare by identity:

console.log({ name: 'Sophie' } === { name: 'Sophie' });  // falseconsole.log([1, 2, 3] === [1, 2, 3]);  // false

Those objects and arrays look identical structurally, but they are separate instances, so strict equality returns false.

This is one of the first places where developers realise that objects in JavaScript are not compared the same way as strings or numbers.


Why Arrays Behave Like Objects

Arrays often feel like a middle case to beginners because they hold lists of values and look simple on the surface. But arrays are still objects in JavaScript.

That means they share the same broad rules:

  • assigning an array does not clone it
  • strict equality checks whether it is the same array instance
  • mutating through one reference affects all references to that array
const originalItems = ['a', 'b'];const copiedItems = originalItems;copiedItems.push('c');console.log(originalItems);  // ['a', 'b', 'c']

Once you understand that arrays live on the object side of the line, this behaviour is much less surprising.


Strings are Primitive, Even Though They Have Methods

This is a small detail that catches some developers.

Strings are primitive values, but JavaScript still lets you call methods on them:

const title = 'checkout';console.log(title.toUpperCase());

That can make strings feel objectlike. They are still primitives. The language simply gives them access to useful methods in a way that feels objectoriented.

For most daytoday work, the important thing is not the deeper implementation detail. It is remembering that strings do not share mutable state the way objects and arrays do.


Reference Types Create Shared‑State Bugs

When developers say "reference type bugs", they usually mean problems caused by several variables or parts of the application pointing to the same object unexpectedly.

For example:

const settings = {  theme: 'dark',};const nextSettings = settings;nextSettings.theme = 'light';

If you expected settings to remain unchanged, you now have a bug. Not because JavaScript misbehaved, but because the object was shared rather than copied.

It is also why object copying patterns matter so much in frontend work, especially once state becomes more central to the application.


Copying an Object is Not the Same as Assigning It

If you need a separate toplevel object, you have to create one deliberately.

const settings = {  theme: 'dark',};const nextSettings = {  ...settings,};

Now the two toplevel objects are different instances.

The same applies to arrays:

const items = ['a', 'b'];const nextItems = [...items];

That said, these are shallow copies. Nested objects can still be shared. That is another important practical detail, but even recognising the toplevel primitive versus reference distinction gets you a long way.


This Affects Function Arguments as Well

The primitive versus reference distinction shows up very clearly when values move into functions.

const updateName = (name: string): void => {  name = 'Ellie';};const updateUser = (user: { name: string }): void => {  user.name = 'Ellie';};

The string example only changes the local parameter variable. The object example mutates the shared object, which is why object arguments often create visible side effects outside the function whilst primitive arguments usually do not.


Why This Matters in Real Front‑End Code

This is not just academic language trivia. The primitiveversusreference split affects:

  • state updates
  • equality checks
  • memoisation
  • function side effects
  • debugging strange data changes

If you are comparing values, copying data, or updating UI state, you are already relying on these rules whether you think about them explicitly or not.

It is worth learning this early. It saves a lot of confusion later when codebases get more complex.


A Useful Question to Ask

When something behaves strangely, ask:

"Am I working with a simple value, or am I working with an objectlike structure that might be shared?"

That question often points you to the bug faster than staring at the syntax alone.


Wrapping up

Primitive values and reference types differ mainly in how they are copied, compared, and mutated. Primitives such as strings and numbers behave like independent values. Objects and arrays behave like shared structures unless you create a new copy deliberately. Once that distinction is clear, a lot of common JavaScript behaviour becomes much easier to reason about.

Key Takeaways

  • Strings, numbers, booleans, null, and undefined are primitive values.
  • Objects, arrays, and functions live on the object side of JavaScript and can be shared by reference.
  • Primitive assignment creates an independent value relationship; object assignment creates shared access to the same underlying object.
  • Strict equality compares primitives by value but objects by identity.
  • Many frontend state bugs come from treating objects as though assignment copied them automatically.

Once you understand which side of that line a value lives on, a lot of JavaScript stops feeling inconsistent and starts feeling predictable.


Categories:

  1. Development
  2. Front‑End Development
  3. Guides
  4. JavaScript