anila.

JavaScript Data Types, Equality and Comparisons.

author avatar of anila website

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:

  1. 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
  2. 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
  3. 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:

  1. 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]
      
  2. 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)
      
  3. 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:

  1. 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
  2. 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!
  3. 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 to NaN.

    • It correctly distinguishes between +0 and 0.

    • 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
      
  4. Same-Value-Zero Equality

    • This algorithm is not exposed as a public method but is used internally by data structures like Map and Set to check for uniqueness. It is identical to Object.is() except that it considers +0 and 0 to be the same.

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

CharacteristicPrimitive TypesReference Types (Objects, Arrays)
Value StoredThe actual data valueA reference (memory address) to the data
AssignmentCopies the value (creates a new, independent copy)Copies the reference (both variables point to the same data)
MutabilityImmutable (operations create new values)Mutable (the original data can be modified directly)
MemoryPrimarily on the StackReference on the Stack, actual data on the Heap
ShallowCompares if values are equalCompares 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.

contact
contact icon
contact iconcontact iconcontact iconcontact iconcontact icon

Feel free to follow me or reach out anytime! Open to work opportunities, collaborations, and connections.

Copyright © anila. All rights reserved.