Skip to main content

[Medium] πŸ“„ Primitive vs Reference Types

1. What are Primitive Types and Reference Types?​

What are Primitive Types and Reference Types?

JavaScript data types can be grouped into two categories: primitive types and reference types. They differ fundamentally in memory behavior and passing semantics.

Primitive Types​

Characteristics:

  • Stored as direct values (commonly conceptualized in stack)
  • Passed by value copy
  • Immutable

7 primitive types:

// 1. String
const str = 'hello';

// 2. Number
const num = 42;

// 3. Boolean
const bool = true;

// 4. Undefined
let undef;

// 5. Null
const n = null;

// 6. Symbol (ES6)
const sym = Symbol('unique');

// 7. BigInt (ES2020)
const bigInt = 9007199254740991n;

Reference Types​

Characteristics:

  • Objects are allocated in heap memory
  • Variables hold references (addresses)
  • Mutable

Examples:

// 1. Object
const obj = { name: 'John' };

// 2. Array
const arr = [1, 2, 3];

// 3. Function
const func = function () {};

// 4. Date
const date = new Date();

// 5. RegExp
const regex = /abc/;

// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();

2. Call by Value vs Call by Reference​

Call by Value vs Call by Reference

Call by Value (primitive behavior)​

Behavior: value is copied; editing the copy does not affect the original.

let a = 10;
let b = a; // copy value

b = 20;

console.log(a); // 10
console.log(b); // 20

Memory diagram:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stack β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ a: 10 β”‚ <- independent value
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ b: 20 β”‚ <- independent value after copy/update
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Reference behavior (objects)​

Behavior: reference is copied; both variables can point to the same object.

let obj1 = { name: 'John' };
let obj2 = obj1; // copy reference

obj2.name = 'Jane';

console.log(obj1.name); // 'Jane'
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true

Memory diagram:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Stack β”‚ β”‚ Heap β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ obj1 ───┼───────────────────>β”‚ { name: 'Jane' } β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ β”‚ β”‚
β”‚ obj2 ───┼───────────────────>β”‚ (same object) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

3. Common Quiz Questions​

Common quiz questions

Question 1: passing primitive values​

function changeValue(x) {
x = 100;
console.log('Inside function x:', x);
}

let num = 50;
changeValue(num);
console.log('Outside function num:', num);
Click to view answer
// Inside function x: 100
// Outside function num: 50

Explanation:

  • num is primitive (Number)
  • function argument gets a copied value
  • changing x does not change num
// flow
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50 (copy)
x = 100; // only x changes
console.log(num); // still 50

Question 2: passing objects​

function changeObject(obj) {
obj.name = 'Changed';
console.log('Inside function obj.name:', obj.name);
}

let person = { name: 'Original' };
changeObject(person);
console.log('Outside function person.name:', person.name);
Click to view answer
// Inside function obj.name: Changed
// Outside function person.name: Changed

Explanation:

  • person is a reference type (Object)
  • function argument copies the reference
  • obj and person point to the same object
// memory sketch
let person = { name: 'Original' }; // heap @0x001
changeObject(person); // obj -> @0x001
obj.name = 'Changed'; // mutate @0x001
console.log(person.name); // reads from @0x001

Question 3: reassignment vs property mutation​

function test1(obj) {
obj.name = 'Modified'; // mutate property
}

function test2(obj) {
obj = { name: 'New Object' }; // reassign local parameter
}

let person = { name: 'Original' };

test1(person);
console.log('A:', person.name);

test2(person);
console.log('B:', person.name);
Click to view answer
// A: Modified
// B: Modified (not 'New Object')

Explanation:

test1: property mutation

function test1(obj) {
obj.name = 'Modified'; // mutates original object
}

test2: reassignment

function test2(obj) {
obj = { name: 'New Object' }; // changes only local binding
}
// person still points to original object

Memory sketch:

// before test1
person ---> { name: 'Original' }
obj ---> { name: 'Original' } (same)

// after test1
person ---> { name: 'Modified' }
obj ---> { name: 'Modified' } (same)

// inside test2
person ---> { name: 'Modified' } (unchanged)
obj ---> { name: 'New Object' } (new object)

// after test2
person ---> { name: 'Modified' }
// local obj is gone

Question 4: array passing​

function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}

function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}

let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);

reassignArray(numbers);
console.log('4:', numbers);
Click to view answer
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]

Explanation:

  • modifyArray: mutates original array
  • reassignArray: only rebinds local parameter

Question 5: equality comparison​

// primitives
let a = 10;
let b = 10;
console.log('A:', a === b);

// references
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
Click to view answer
// A: true
// B: false
// C: true

Explanation:

Primitives compare by value; objects compare by reference.

obj1 === obj2; // false (different references)
obj1 === obj3; // true (same reference)

4. Shallow Copy vs Deep Copy​

Shallow copy vs deep copy

Shallow Copy​

Definition: only top level is copied; nested objects remain shared references.

Method 1: spread operator​

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = { ...original };

copy.name = 'Jane';
console.log(original.name); // 'John'

copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung' (affected)

Method 2: Object.assign()​

const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);

copy.name = 'Jane';
console.log(original.name); // 'John'

Method 3: array shallow copy​

const arr1 = [1, 2, 3];

const arr2 = [...arr1];
const arr3 = arr1.slice();
const arr4 = Array.from(arr1);

arr2[0] = 999;
console.log(arr1[0]); // 1

Deep Copy​

Definition: all levels are copied recursively.

Method 1: JSON.parse(JSON.stringify(...))​

const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};

const copy = JSON.parse(JSON.stringify(original));

copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'

copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming']

Limitations:

const obj = {
date: new Date(), // -> string
func: () => {}, // ignored
undef: undefined, // ignored
symbol: Symbol('test'), // ignored
regexp: /abc/, // -> {}
circular: null, // circular reference throws
};
obj.circular = obj;

JSON.parse(JSON.stringify(obj)); // error or data loss

Method 2: structuredClone()​

const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};

const copy = structuredClone(original);

console.log(copy.date instanceof Date); // true

Pros:

  • Supports Date, RegExp, Map, Set, etc.
  • Supports circular references
  • Usually better performance than manual deep clone

Limitations:

  • Does not clone functions
  • Does not clone Symbol values in all usage patterns

Method 3: recursive deep clone​

function deepClone(obj) {
if (obj === null || typeof obj !== 'object') {
return obj;
}

if (Array.isArray(obj)) {
return obj.map((item) => deepClone(item));
}

if (obj instanceof Date) {
return new Date(obj);
}

if (obj instanceof RegExp) {
return new RegExp(obj);
}

const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}

return cloned;
}

const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading'],
date: new Date(),
};

const copy = deepClone(original);
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'

Method 4: Lodash​

import _ from 'lodash';

const original = {
name: 'John',
address: { city: 'Taipei' },
};

const copy = _.cloneDeep(original);

Shallow vs Deep comparison​

FeatureShallow CopyDeep Copy
Copy depthTop level onlyAll levels
Nested objectsShared referencesFully independent
PerformanceFasterSlower
Memory useLowerHigher
Use caseSimple structuresComplex nested structures

5. Common Pitfalls​

Common pitfalls

Pitfall 1: expecting primitive args to mutate outer value​

function increment(num) {
num = num + 1;
return num;
}

let count = 5;
increment(count);
console.log(count); // 5

count = increment(count);
console.log(count); // 6

Pitfall 2: expecting reassignment to replace outer object​

function resetObject(obj) {
obj = { name: 'Reset' }; // local rebinding only
}

let person = { name: 'Original' };
resetObject(person);
console.log(person.name); // 'Original'

// correct approach 1: mutate property
function resetObject1(obj) {
obj.name = 'Reset';
}

// correct approach 2: return new object
function resetObject2(obj) {
return { name: 'Reset' };
}
person = resetObject2(person);

Pitfall 3: assuming spread is deep copy​

const original = {
user: { name: 'John' },
};

const copy = { ...original }; // shallow

copy.user.name = 'Jane';
console.log(original.user.name); // 'Jane'

const deep = structuredClone(original);

Pitfall 4: misunderstanding const​

const obj = { name: 'John' };

// obj = { name: 'Jane' }; // TypeError

obj.name = 'Jane'; // allowed
obj.age = 30; // allowed

const immutableObj = Object.freeze({ name: 'John' });
immutableObj.name = 'Jane';
console.log(immutableObj.name); // 'John'

Pitfall 5: shared reference in loops​

const arr = [];
const obj = { value: 0 };

for (let i = 0; i < 3; i++) {
obj.value = i;
arr.push(obj); // same object reference each time
}

console.log(arr);
// [{ value: 2 }, { value: 2 }, { value: 2 }]

const arr2 = [];
for (let i = 0; i < 3; i++) {
arr2.push({ value: i }); // new object each iteration
}

console.log(arr2);
// [{ value: 0 }, { value: 1 }, { value: 2 }]

6. Best Practices​

Best practices

// 1. choose explicit copy strategy
const original = { name: 'John', age: 30 };

const copy1 = { ...original }; // shallow
const copy2 = structuredClone(original); // deep

// 2. avoid mutation side effects in functions
function addItem(arr, item) {
return [...arr, item]; // immutable style
}

// 3. use const to prevent accidental rebinding
const config = { theme: 'dark' };

// 4. use Object.freeze for immutable constants
const constants = Object.freeze({
PI: 3.14159,
MAX_SIZE: 100,
});

❌ Avoid​

function increment(num) {
num++; // ineffective for outer primitive
}

const copy = { ...nested }; // not deep copy

for (let i = 0; i < 3; i++) {
arr.push(obj); // same object reference reused
}

7. Interview Summary​

Interview summary

Quick memory​

Primitive:

  • String, Number, Boolean, Undefined, Null, Symbol, BigInt
  • Passed by value
  • Immutable

Reference:

  • Object, Array, Function, Date, RegExp, etc.
  • Variable stores reference to heap object
  • Mutable

Sample interview answer​

Q: Is JavaScript call-by-value or call-by-reference?

JavaScript is call-by-value for all arguments. For objects, the value being copied is the reference (memory address).

  • Primitive arguments: copying value does not affect outer variable.
  • Object arguments: copying reference allows mutation of the same object.
  • Reassigning the local parameter does not change the outer binding.

Reference​