Data types in JavaScript
JavaScript data types split into two groups: seven primitives (immutable, copied by value) and objects (mutable, copied by reference).
Theory
TL;DR
- Primitives are like printed postcards: copy one and you get an independent duplicate.
- Objects are like a shared Google Doc: all "copies" point to the same underlying data.
- Seven primitives:
undefined,null,boolean,number,string,symbol,bigint. - Objects include plain objects, arrays, functions, dates, regular expressions, and more.
typeof nullreturns"object", not"null". A known bug from JavaScript's first version.
Quick Example
// Primitives: assignment copies the value
let a = 5;
let b = a; // b gets its own copy of 5
b = 10;
console.log(a); // 5, unchanged
// Objects: assignment copies the reference
let x = { val: 5 };
let y = x; // y points to the same object
y.val = 10;
console.log(x.val); // 10, x changed tooThese two snippets explain most of the bugs developers file on this topic.
Key Difference
When you assign a primitive, JavaScript copies the actual value into a new slot in memory. The two variables become independent. Objects work differently: the variable stores a reference (a memory address), not the data itself. Assign that variable to another and both point at the same heap object. Mutate one, and the change shows up through both.
When to Use
- Loop counter →
numberprimitive. - User's display name →
stringprimitive. - User profile with multiple fields → object.
- Toggle flag in React state →
booleanprimitive. - Form data in React state → object.
- Integers beyond 2^53 - 1 →
bigint.
Memory Model
V8 stores primitives on the stack for fast, predictable access. Objects live on the heap, and only the pointer lives on the stack. That pointer is what gets copied during assignment, which is why two variables can unknowingly share the same heap data. Object.create and similar APIs manipulate those heap structures directly.
Common Mistakes
Mistake 1: treating arrays like primitives
let arr1 = [1, 2];
let arr2 = arr1; // reference copy, not a new array
arr2.push(3);
console.log(arr1); // [1, 2, 3], not what you expectedArrays are objects. Fix: let arr2 = [...arr1] or arr1.slice().
Mistake 2: trying to mutate a string character by character
let s = "hello";
s[0] = "H"; // no error, but nothing changes
console.log(s); // "hello"Strings are immutable. To change: s = "H" + s.slice(1).
Mistake 3: comparing objects with ===
console.log({} === {}); // false, different references
console.log([1] === [1]); // falseObjects compare by reference, not content. For structural comparison use JSON.stringify or lodash.isEqual. For more on how == behaves with different types, see type coercion in JavaScript.
Mistake 4: typeof null
typeof null === "object" // true
typeof null === "null" // falseThis is a bug from Netscape's JavaScript 1.0. The internal type tag for null collided with the object type tag. Proposals to fix it were rejected over backward compatibility. Check for null explicitly: value === null.
Mistake 5: BigInt in JSON
const id = 123n;
JSON.stringify(id); // TypeErrorJSON has no BigInt support. Convert before serializing: id.toString(), or pass a replacer function to JSON.stringify.
Real-world Usage
- React: props as objects
{ userId: 123, name: "Alice" }; list keys as primitiveskey={id}. - Express:
req.bodyis an object parsed from JSON;res.status(200)takes a number primitive. - Node.js fs: the
errparameter in callbacks is eithernull(primitive) or anErrorobject. - Redux: state is an object tree; action
typeis a string primitive like"INCREMENT". - Lodash:
_.cloneDeep(obj)deep-copies nested objects without sharing any references.
Follow-up Questions
Q: What happens if you pass a primitive to a function and modify it inside?
A: The function gets a copy. Changes inside don't affect the original. With objects the function gets the reference, so mutations are visible to the caller.
Q: Why does typeof null return "object"?
A: It's a bug from JavaScript's first release at Netscape. The original type tag for null matched the object type tag. Multiple proposals to fix it failed because the change would break existing websites.
Q: Name all seven primitives.
A: undefined, null, boolean, number, string, symbol (ES2015), bigint (ES2020).
Q: How is BigInt different from Number?
A: Number is a 64-bit float with a safe integer limit of 2^53 - 1. BigInt handles integers of arbitrary size. They don't coerce into each other implicitly, so 1n + 1 throws a TypeError.
Q (senior): An object is passed to an async callback. The caller mutates the object before the callback runs. What does the callback see?
A: The mutated version. The callback holds a reference to the same heap object, not a snapshot. This is a common cause of stale-data bugs in async code where a shared object is modified between scheduling and execution.
Examples
Primitives vs objects when passed to a function
function addTen(n) {
n = n + 10; // modifies a local copy only
}
function addScore(obj) {
obj.score = 100; // modifies the actual object
}
let num = 5;
addTen(num);
console.log(num); // 5, unchanged
let user = { name: "Alice" };
addScore(user);
console.log(user.score); // 100, changedPass a primitive and any mutation stays inside the function. Pass an object and the caller sees every change. I've watched this trip up junior developers who assumed JavaScript always makes a copy of what you pass in.
Extracting a primitive from a React prop
function UserProfile({ user }) {
// Extracting a string primitive from the prop object
const [name, setName] = useState(user.name);
// Editing the input only updates local state
// user.name stays untouched
return <input value={name} onChange={e => setName(e.target.value)} />;
}useState(user.name) copies the string value, not a reference to user. Local edits never touch the original prop. If user itself were passed to useState, any mutation to that prop object would be shared with the parent component.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.