
JavaScript Data Types, Equality and Comparisons.

In JavaScript, understanding how different data types are stored and compared is fundamental to writing reliable code. While comparing simple data like numbers or strings is intuitive, the behavior of objects and arrays can often lead to unexpected results. This article breaks down the core concepts of data types, explains crucial differences between equality algorithms, and clarifies the distinction between value comparisons to help you avoid common issues.
Data Types in JavaScript
JavaScript data types can be broadly divided into two categories: Primitives and Objects (which are reference types).
Primitive Types
A primitive is data that is not an object and has no methods. Primitive types include: String
, Number
, Boolean
, Null
, Undefined
, Symbol
(added in ES6), and BigInt
(added in ES2020). The values of these types are stored directly in the location that the variable accesses.
Core Characteristics of Primitive Types:
-
Immutability: The values of primitive types are immutable. This means that once a primitive value is created, you cannot change that value itself. When you perform an operation on a variable of a primitive type (e.g., adding to a number or concatenating a string), you are actually creating a new value and then pointing the variable to this new value. The original value is not changed.
-
Example:
jsx
let str = "hello"; str.toUpperCase(); // This method returns a NEW string "HELLO" console.log(str); // The original string "hello" is unchanged let str = "hello"; str = str + " world"; // "hello world" is a new string, and str now points to it. // The original "hello" string is not modified. console.log(str); // Outputs: "hello world" let num = 5; let newNum = num + 5; // newNum gets a new value, 10. num remains 5. console.log(num); // Outputs: 5 console.log(newNum); // Outputs: 10
-
-
Compared & Accessed by Value:
When you compare two primitive types, you are comparing their actual values.
-
Example:
jsx
let num1 = 100; let num2 = 100; console.log(num1 === num2); // Outputs: true, because their values are the same.
When you assign a primitive type variable to another variable, a copy of the actual value is given to the new variable. Therefore, both variables have their own independent values, and modifying one does not affect the other.
-
Example:
jsx
let num1 = 100; let num2 = num1; // num2 gets a copy of the value 100. num2 = 200; // Modifying num2. console.log(num1); // Outputs: 100 (num1 is not affected) console.log(num2); // Outputs: 200
-
-
Stored in Stack Memory: The values of primitive types are typically stored directly in the stack memory space of the variable. Stack memory is fast to access and has a fixed size.
Reference Types (Objects)
In JavaScript, an "object" is a standalone entity, with properties and type. It is the only mutable data type. Reference types mainly refer to Object
, Array
, and Function
. Variables of these types store a "reference" or "pointer" to the actual data's location in memory, not the data itself.
Core Characteristics of Reference Types:
-
Mutability: The values of reference types are mutable. You can modify the properties of an object or the elements of an array without creating an entirely new object or array.
-
Example:
jsx
let myObj = { name: "Initial" }; myObj.name = "Changed"; // Directly modifies the name property of the object myObj points to. console.log(myObj.name); // Outputs: "Changed" let myArray = [1, 2]; myArray.push(3); // Directly modifies the array myArray points to. console.log(myArray); // Outputs: [1, 2, 3]
-
-
Compared & Accessed by Reference: When you assign or compare reference type variables, you are working with their "reference" (or memory address), not their actual content. For instance, when assigning a reference type variable to another, what's copied is the "reference" (or memory address) to the object/array in memory. This means both variables point to the same object or array in memory.
-
Example:
jsx
let obj1 = { name: "Alice" }; let obj2 = obj1; // obj2 now holds the same memory reference as obj1 let obj3 = { name: "Alice" }; // A new object, with a new reference in memory console.log(obj1 === obj2); // Outputs: true (they point to the same object) console.log(obj1 === obj3); // Outputs: false (they point to different objects, despite identical content)
-
-
Stored in Heap Memory: The actual content of objects and arrays is stored in heap memory. The variable itself (in stack memory) stores the reference to that data in the heap. Heap memory is larger but can be slower to access.
Objects and Arrays as Reference Types
When you create an object or an array, JavaScript allocates a block of memory to store its content. The variable itself only stores the reference to that memory location.
-
Example:
jsx
// Primitive type let a = 10; let b = a; // b gets a copy of the value of a. b = 20; console.log(a); // Outputs: 10 (a's value is not affected) console.log(b); // Outputs: 20 // Reference type (Object) let obj1 = { name: "Alice", age: 30 }; let obj2 = obj1; // obj2 copies the reference from obj1 (points to the same memory location). obj2.age = 35; // Modifying a property via obj2. console.log(obj1.age); // Outputs: 35 (obj1's property is also changed because they point to the same object) console.log(obj2.age); // Outputs: 35 // Reference type (Array) let arr1 = [1, 2, 3]; let arr2 = arr1; // arr2 copies the reference from arr1. arr2.push(4); // Modifying the array via arr2. console.log(arr1); // Outputs: [1, 2, 3, 4] (arr1 is also changed) console.log(arr2); // Outputs: [1, 2, 3, 4]
As you can see from the example, when you assign one reference type variable to another, you are copying the reference. Thus, both variables point to the same object or array in memory. Modifying the object or array through one variable will affect the other.
Equality Comparisons and Sameness
Comparing primitives is straightforward, but comparing objects requires understanding JavaScript's different equality algorithms.
Shallow Comparison (Comparing by Reference)
When using any standard equality operator ==
(loose equality) or ===
(strict equality) Operators to compare objects or arrays, you are performing a shallow comparison. This means JavaScript is only checking if the variables point to the exact same object in memory; In other words, it only compares if their references (memory addresses) are the same, it does not check if the internal contents of the objects or arrays are identical.
-
Example:
jsx
// Array let arrA = [10, 20]; let arrB = [10, 20]; let arrC = arrA; console.log(arrA === arrB); // Outputs: false console.log(arrA === arrC); // Outputs: true
jsx
// Object let objA = { id: 1, value: "Test" }; let objB = { id: 1, value: "Test" }; let objC = objA; // objC and objA point to the same object. console.log(objA === objB); // Outputs: false (Even though the content is the same, they are different objects in memory with different references) console.log(objA == objB); // Outputs: false (Same as above) console.log(objA === objC); // Outputs: true (objA and objC point to the same object, so their references are the same) console.log(objA == objC); // Outputs: true (Same as above)
Therefore, even if two different objects or arrays have the exact same properties/elements and values, a shallow comparison will consider them not equal because they are separate instances in memory.
Equality Operators
JavaScript provides four different equality algorithms:
-
Loose Equality (
==
)-
This operator compares two values for equality after performing type coercion if they are not of the same type.
-
Because its rules for coercion can be complex and unintuitive, it is highly recommended to avoid using loose equality.
-
Examples:
jsx
console.log(1 == "1"); // true console.log(true == 1); // true console.log(null == undefined); // true console.log(0 == false); // true
-
-
Strict Equality (
===
)-
This is the most commonly used comparison operator. It compares two values without performing any type coercion. If the types are different, it immediately returns
false
. -
For objects, it performs a shallow, reference-based comparison.
-
One edge case: It considers
NaN
to be unequal to everything, including itself. -
Examples:
jsx
console.log(1 === "1"); // false (different types) console.log(true === 1); // false (different types) console.log(null === undefined); // false (different types) console.log(NaN === NaN); // false!
-
-
Same-Value Equality (
Object.is()
)-
This algorithm was introduced in ES6. It behaves almost identically to strict equality (
===
) but resolves two key edge cases. -
It correctly determines that
NaN
is equal toNaN
. -
It correctly distinguishes between
+0
and0
. -
Examples:
console.log(Object.is(1, "1")); // false console.log(Object.is(NaN, NaN)); // true console.log(Object.is(+0, -0)); // false // For comparison, strict equality treats +0 and -0 as equal: console.log(+0 === -0); // true
-
-
Same-Value-Zero Equality
- This algorithm is not exposed as a public method but is used internally by data structures like
Map
andSet
to check for uniqueness. It is identical toObject.is()
except that it considers+0
and0
to be the same.
- This algorithm is not exposed as a public method but is used internally by data structures like
Deep Comparison (Comparing by Value)
A deep comparison involves recursively comparing the actual contents of two objects or arrays, rather than just their references. If all properties/elements and their corresponding values are equal for both objects or arrays (and this check is performed recursively for nested objects/arrays), then they are considered deeply equal.
JavaScript does not have a built-in operator for deep comparison. You need to implement a deep comparison function yourself or use a third-party library (like Lodash's _.isEqual()
method).
Simple Deep Comparison Implementation (Example):
The following is a very simplified example of a deep comparison function, meant only to illustrate the concept. It does not handle all edge cases (e.g., null
, undefined
property values, Date objects, regular expressions, functions, etc..).
jsx
function deepCompare(obj1, obj2) { // Check if they are not objects or are null if (typeof obj1 !== 'object' || obj1 === null || typeof obj2 !== 'object' || obj2 === null) { return obj1 === obj2; // For non-objects or null, perform a direct comparison } const keys1 = Object.keys(obj1); const keys2 = Object.keys(obj2); // Check if the number of properties is the same if (keys1.length !== keys2.length) { return false; } // Compare property values one by one (recursively) for (let key of keys1) { if (!keys2.includes(key) || !deepCompare(obj1[key], obj2[key])) { return false; } } return true; } let objX = { name: "Bob", details: { age: 25, city: "New York" } }; let objY = { name: "Bob", details: { age: 25, city: "New York" } }; let objZ = { name: "Bob", details: { age: 30, city: "London" } }; console.log("Deep compare objX and objY:", deepCompare(objX, objY)); // Outputs: Deep compare objX and objY: true console.log("Shallow compare objX and objY:", objX === objY); // Outputs: Shallow compare objX and objY: false console.log("Deep compare objX and objZ:", deepCompare(objX, objZ)); // Outputs: Deep compare objX and objZ: false let arrX = [1, [2, 3], { a: 4 }]; let arrY = [1, [2, 3], { a: 4 }]; let arrZ = [1, [2, 3], { a: 5 }]; console.log("Deep compare arrX and arrY:", deepCompare(arrX, arrY)); // Outputs: Deep compare arrX and arrY: true console.log("Shallow compare arrX and arrY:", arrX === arrY); // Outputs: Shallow compare arrX and arrY: false console.log("Deep compare arrX and arrZ:", deepCompare(arrX, arrZ)); // Outputs: Deep compare arrX and arrZ: false
When to use deep comparison?
You need to perform a deep comparison when you want to determine if two different object or array instances have the same content. For example:
- When comparing previous and next application states.
- In testing, to verify if a function's return value matches the expected outcome (when the result is an object or array).
- To avoid unnecessary duplicate operations if new and old data content is the same.
Summary
Characteristic | Primitive Types | Reference Types (Objects, Arrays) |
---|---|---|
Value Stored | The actual data value | A reference (memory address) to the data |
Assignment | Copies the value (creates a new, independent copy) | Copies the reference (both variables point to the same data) |
Mutability | Immutable (operations create new values) | Mutable (the original data can be modified directly) |
Memory | Primarily on the Stack | Reference on the Stack, actual data on the Heap |
Shallow | Compares if values are equal | Compares if references (memory addresses) are the same |
Deep Comparison | (Not applicable, value comparison is deep) | Recursively compares the contents of the object/array |
Understanding the differences between reference types, shallow comparison, and deep comparison is essential for writing correct and efficient JavaScript code, especially when dealing with complex data structures.
Conclusion
In short, the key takeaway is that JavaScript treats primitives and objects very differently. Primitives are compared by their value, making comparisons simple and predictable. Objects, however, are compared by their reference (their address in memory). This means two objects with identical content will not be considered equal by standard operators (=== or Object.is()). To compare the internal content of two separate objects, you must perform a deep comparison, which requires a custom recursive function or a library. Understanding this distinction is crucial for managing state and avoiding subtle bugs in your applications.