Teleporting Data: The Magic of Serializing and Deserializing in JavaScript! 🚀✨ #chaiCode

Teleporting Data: The Magic of Serializing and Deserializing in JavaScript! 🚀✨ #chaiCode

Have you ever wondered about teleporting a person or an object from one place to another? It seems like magic, right? But if I say we can actually do this, you wouldn’t believe me, would you?

Today, I am going to explain how we can teleport data in computer networks with a real-world example. So, let’s begin the magic!

Before we dive deep into the blog, we need to understand how JavaScript stores data in memory.

Understanding How JavaScript Stores Data in Memory

Javascript Engine store their data in two places; the Stack Memory and Heap Memory.

Stack Memory

A stack is a linear data structure that stores data in a specific order or sequence, following the FILO (First In, Last Out) or LIFO (Last In, First Out) principle.

A linear data structure is a way of organizing data in a sequential order. This makes it easy to access and manipulate the data. Examples: Array, Stack, Queue.

A stack is used for static memory allocation, which means the size and location of the memory are fixed at compile time.

In JavaScript, primitive values such as strings, numbers, and booleans are stored in the stack.

An example of a stack in the real world is a stack of plates. In India, when we attend a wedding dinner, we often see a pile of plates. Everyone takes a plate from the top, which means the last plate placed on the stack is picked first. In other words, the first plate placed at the bottom is picked last after all the plates on top have been taken.

Heap Memory

A heap is a tree-based data structure.

Heap memory in JavaScript is dynamic memory where we can store data that can change in size. Examples include arrays, objects, and functions.

Objects and arrays are mutable and have a variable size, so they need to be stored and accessed in the heap. The heap is more flexible but also slower.

How Memory Works in JavaScript

With Primitive Data Types

//we declare the variable name and its value is chaiAurCode
let name = "chaiAurCode";

When we create a variable in JavaScript, the JS engine stores the variable in the stack along with its value (“chaiAurCode”) because the variable is of type string, which is a primitive data type.

let name = "chaiAurCode";
let petName = name;
petName = "codeInHindi";

//output :
console.log(name) //chaiAurCode
console.log(petName ) //codeInHindi
  • In the above JavaScript code, we created the variable name and assigned it the string "chaiAurCode". Since strings are primitive data types, they are stored in stack memory.

  • Next, we created the variable petName and assigned it the value of name.

  • Since petName is a primitive data type (string), JavaScript did not copy the reference. Instead, it created a new copy of the string "chaiAurCode" in stack memory.

  • In the next line, we modified the code by assigning the new string "codeInHindi" to the variable petName.

  • Since petName had its own copy of "chaiAurCode", this modification does not affect the original name variable.

  • Now, petName points to "codeInHindi" in stack memory, while name still holds "chaiAurCode".

As we saw in the above example, when a variable is declared with a primitive data type (such as string, number, or boolean), the JavaScript engine stores it in stack memory. Each variable gets its own memory space, so modifying one variable does not affect the original value stored in the stack.

With Non-Primitive Data Types

let person = {
    name: "ChaiAurCode",
    age: 24,
    isMale: true,
};
console.log(person);  // {name: 'ChaiAurCode', age: 24, isMale: true}

let newPerson = person;
newPerson.name = "Anurag Dubey";
console.log(person); //  {name: 'Anurag Dubey', age: 24, isMale: true}

In the above example, we created a copy of the person object in the newPerson object and assigned the value of the person object. However, we actually created a shallow copy of person. When we tried to change the name property of newPerson, why did it also affect the person object?

Now, if you want to understand the above concept, we need to understand how the JavaScript engine assigns memory to non-primitive data types.

The JavaScript engine stores non-primitive data types (such as objects and arrays) in heap memory and stores the variable in stack memory, which holds a reference to the heap memory location.

In the above example, when we created a copy of the person object in the newPerson object, we made a shallow copy. As a result, the newPerson object also refers to the same location in heap memory.

Now, whenever we modify the values of the newPerson object, the JavaScript engine also modifies the original person object because both objects share the same memory location (reference) in the heap.

Shallow Copy in Memory

A shallow copy is a copy of an object where only the references to nested objects are copied, not the actual nested objects themselves. This means that while the top-level properties are copied, any nested objects or arrays still share the same reference in memory.

How It Works in Memory

  • The shallow copy creates a new object in stack memory.

  • However, if the original object contains nested objects or arrays, only their references (memory addresses) are copied to the new object.

  • As a result, changes made to the nested objects in the copied object also affect the original object since both share the same memory location in heap memory.

Now, a doubt arises! If we want to copy an object or array without modifying the original object or array in JavaScript, how can we do that? Is it possible in JavaScript or not?

Serialization in JavaScript: Converting Data for Storage and Transfer

Serialization starts with creating a deep copy of nested non-primitive data types (such as objects and arrays). Then, we convert the deep copy into a string, meaning we transform the non-primitive data type into a primitive data type (such as a string).

As we saw earlier, the JavaScript engine assigns a copy of the string to a new variable in stack memory. After that, we convert the primitive data type back into a non-primitive data type—a process called deserialization.

Once we obtain the deserialized object or array, we can modify it without affecting the original object or array. Additionally, the new deserialized object or array is assigned a new location in heap memory.

JSON.stringify()

The JSON.stringify() method converts a JavaScript object or array into a JSON-formatted string. This process is called serialization, and it allows data to be easily stored, transferred, or sent over a network.

let person = {
    name: "ChaiAurCode",
    age: 24,
    isMale: true,
};

// Serialization with the help of built-in methods.
let serializedObj = JSON.stringfy(person);
console.log(serializedObj) // {"name":"ChaiAurCode","age":24,"isMale":true}

JSON.parse()

JSON.parse() method that converts a JSON-formatted string into a JavaScript object. This process is called deserialization, as it transforms data from a string format back into a usable object or array.

let serializedObj = '{"name":"ChaiAurCode","age":24,"isMale":true}';
let user = JSON.parse(serializedObj); 
//output
// {
//    name: "ChaiAurCode",
//    age: 24,
//    isMale: true,
// }

Example: Storing and Retrieving a JavaScript Object in Local Storage

In local storage, if we need to store a string, we can use the setItem method. To retrieve the data from local storage, we use the getItem method.

let name = "chaiAurCode";
//setItem ==> key and value
localStorage.setItem("name", name);

//get the data from localstorage
localStorage.getItem("name"); // output => chaiAurCode

Now, if we need to store an object in local storage, let's see how we can do it in the example below.

let person = {
    name: "ChaiAurCode",
    age: 24,
    isMale: true,
};

//set the data into local storage.
localStorage.setItem("personData", person);

localStorage.getItem("personData"); // output => [Object Object]

Why do we get [object Object] instead of the person object?

This happens because [object Object] is the string representation of an object instance when it is implicitly converted to a string. At the time of storing the data, the object's actual value was not properly serialized, leading to data loss.

The correct way to do this is to first serialize the object, meaning we convert the object into a string. Then, we can store it in local storage.

  • JSON.stringify() : Converts any object value into a string JSON (serialization).

  • JSON.parse() : Turns a JSON string into its corresponding object or value (deserialization).

Now, we can try using the setItem method again with JSON.stringify(). This allows us to easily convert a JavaScript object into a JSON string and store the data in local storage.

Here is a quick example:

let person = {
    name: "ChaiAurCode",
    age: 24,
    isMale: true,
};

//set the data into local storage.
localStorage.setItem("personData", JSON.stringify(person));

Now, if we try to retrieve the data from local storage without deserialization, we will get a JSON string instead of an object. This makes sense because we originally stored the data as a JSON string in local storage.

let newObject = localStorage.getItem("personData");
console.log(newObject); //output => {"name":"ChaiAurCode","age":24,"isMale":true}

console.log(JSON.parse(newObject)); //output => {name: 'ChaiAurCode', age: 24, isMale: true}

Here, we retrieved our previously stored JavaScript object using the getItem method on the localStorage object and saved it in a variable. Next, we parsed the JSON string into a JavaScript object and finally logged it to the console.

Creating a Custom Serialization and Deserialization Method in JavaScript

Fellow developers, let's try to create a custom method for serialization and deserialization of non-primitive data types in JavaScript.

Overview

Serialization and deserialization are essential when storing or transmitting non-primitive data types such as objects and arrays. JavaScript provides built-in methods like JSON.stringify() and JSON.parse(), but they have limitations—for example, they do not support Date, RegExp, or functions.

To overcome these limitations, we define custom methods:

  • customSerialize(obj): Converts JavaScript objects into a string format while preserving special types like Date and RegExp.

  • customDeserialize(str): Converts the serialized string back into a JavaScript object, restoring special types.

function customSerialize(obj) {
  // Handle null values
  if (obj === null) return 'null'; 

  // Handle primitives (numbers, booleans, strings)
  if (typeof obj !== 'object') {
    // Escape quotes in strings (e.g., "Hello" becomes "\"Hello\"")
    return typeof obj === 'string' ? `"${obj.replace(/"/g, '\\"')}"` : `${obj}`;
  }

  // Handle arrays (recursive serialization)
  if (Array.isArray(obj)) {
    const elements = obj.map(e => customSerialize(e)).join(','); // Serialize each element
    return `[${elements}]`; // Wrap in array brackets
  }

  // Handle Date objects
  if (obj instanceof Date) {
    return `{"__type__":"Date","value":"${obj.toISOString()}"}`;
  }

  // Handle RegExp objects
  if (obj instanceof RegExp) {
    return `{"__type__":"RegExp","source":"${obj.source}","flags":"${obj.flags}"}`;
  }

  // Handle plain objects (recursive serialization)
  const entries = [];
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      const serializedKey = `"${key.replace(/"/g, '\\"')}"`; // Escape quotes in keys
      entries.push(`${serializedKey}:${customSerialize(obj[key])}`);
    }
  }
  return `{${entries.join(',')}}`; // Wrap in object braces
}
function customDeserialize(str) {
  // Parse the string into a JavaScript object (using eval for simplicity)
  // WARNING: eval is unsafe for untrusted input!
  const parsed = eval(`(${str})`); // Wrapped in parentheses to parse as object
  return revive(parsed); // Revive special types
}

// Reviver function to reconstruct objects
function revive(obj) {
  if (typeof obj === 'object' && obj !== null) {
    // Handle special types (Date/RegExp)
    if ('__type__' in obj) {
      switch (obj.__type__) {
        case 'Date':
          return new Date(obj.value); // Recreate Date
        case 'RegExp':
          return new RegExp(obj.source, obj.flags); // Recreate RegExp
        default:
          return obj;
      }
    }
    // Recursively revive nested properties
    for (const key in obj) {
      obj[key] = revive(obj[key]);
    }
  }
  return obj;
}

Conslusion

In this blog, we learned how JavaScript manages the memory of primitive and non-primitive data types. We explored topics like serialization and deserialization in JavaScript using built-in methods, attempted to create our own custom methods for serialization and deserialization, and examined a real-world example of local storage in depth.

Â