Green pattern
Cover image for ron

Ron Northcutt Verified userVerified user

Head of DevRel

Appsmith

Copy Or Reference? Understanding Javascript Object Assignments

When you first start programming, it seems pretty simple: create a variable, assign it a value, do stuff with that variable, and then go home. Easy peasy.

As your skill and ability increase, you will start to notice lots of nuances and details in the tools. For example, larger groups of state or data need to be managed with more advanced apps. Typically, this is done by creating or updating a structured object. But how should that object be structured? How should that data be organized? And more importantly, how does this object live in memory, and what can I expect from it?

When dealing with objects in JavaScript, how you assign them to variables or properties can have significant implications for your app's behavior. The default approach is to assign the value using the "=" operator. This is called direct assignment. However, another approach is to make a copy using one of several approaches. 

While these approaches have their own names, I like to think of them this way:

  1. Reference - using assignment operators like =
  2. Array Copy - using the spread operator [...]
  3. Object Copy - using JSON functions, Lodash, or structuredClone(obj)

When assigning objects in JavaScript, your choice can lead to different outcomes, particularly regarding how changes to the object are handled. Understanding the difference between these approaches is crucial for effective state management. Let's look at how these different approaches work. 

Direct Assignment aka Reference

Direct assignment (this.list = listData;) sets this.list to reference the same object as listData. That means we are just dealing with a pointer to that object in memory. So, we say that both this.list and listData are two different labels for the same object.

This approach has its own set of implications:

  • Shared Reference: this.list and listData will refer to the same object. Any changes to this.list will directly affect listData.
  • Single Source of Truth: Useful when you want this.list to be directly linked to listData, maintaining consistency between them.

Example:

const listData = { a: 1, b: 2 }; 
this.list = listData; // References the same object 
this.list.a = 3; 
console.log(listData.a); // Output: 3

In this example, modifying this.list also changes listData, as both reference the same object. This causes lots of problems when you don't expect that behavior. However, it is still very helpful, especially when managing state. You can use block-level variables to reference specific parts of the global state easily.

When to Use This Approach:

  • When you want any changes to this.list to be reflected in listData immediately.
  • When managing a single source of truth is crucial for your application logic.

This is why I think of this as a reference - it reminds me that this is a shared object, not a unique one.

Two types of Copy

When working with objects and arrays in JavaScript, the terms "shallow copy" and "deep copy" refer to different ways of duplicating these data structures. Understanding these concepts is vital for effective state management and avoiding unintended side effects in your code. For this reason, I like to think of these methods as either for flat (array) data or nested (object) data.

Shallow Copy aka Array Copy

A shallow copy of an object or array is a copy where only the top-level properties are duplicated. If these properties are references to other objects or arrays, the references are copied, but the nested objects or arrays are not. This means changes to the nested objects or arrays in the copied object will also affect the original object.

Example:

let original = {
    name: 'Alice',
    address: {
        city: 'Wonderland',
        zip: '12345'
    }
};
// Creating a shallow copy using the spread operator
let shallowCopy = { ...original };
shallowCopy.name = 'Peter'; // This changes only the shallow copy
shallowCopy.address.city = 'Neverland'; // This changes both the shallow copy and the original
console.log(original.name); // Output: 'Alice'
console.log(original.address.city); // Output: 'Neverland'

In this example, changing shallowCopy.name does not affect original.name, but changing shallowCopy.address.city does affect original.address.city because the address object is shared between the original and the shallow copy.

Two ways to Create a Shallow Copy:

  1. Spread operator: The spread operator is the simplest way to make a shallow copy (array copy) and is a very elegant approach.

    let shallowCopy = { ...original };
  2. Lodash: The Lodash library provides a _.clone method that does the same thing. I prefer this approach because it will be easier for another developer to understand the code, especially if they aren't familiar with the spread operator in Javascript. 

    const _ = require('lodash');
            let deepCopy = _.clone(original);        

Pro tip: Appsmith includes the Lodash library by default in all apps, so take advantage of it!

When to Use This Approach:

  • Use when you only need to copy the top-level properties.
  • You are okay with nested objects being shared. 

This is often sufficient for simple state updates or when working with immutable data structures. This is why I think of it as "Array Copy" - because I only want to use the shallow copy for arrays or flat data structures. I prefer to use the Lodash method clone() as my default because I want to make sure that the next developer understands the code, but if you are the primary maintainer, you may prefer the {...} for aesthetic reasons.

Deep Copy aka Object Copy

A deep copy of an object or array is a copy where all levels of the object or array are duplicated. This means that all nested objects and arrays are also copied, and the new copy is completely independent of the original. Changes to any part of the copied object or array will not affect the original.

Example:

const original = {
    name: 'Alice',
    address: {
        city: 'Wonderland',
        zip: '12345'
    }
};
// Creating a deep copy using a custom function or a library like Lodash
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.name = 'Peter'; // This changes only the deep copy
deepCopy.address.city = 'Neverland'; // This changes only the deep copy
console.log(original.name); // Output: 'Alice'
console.log(original.address.city); // Output: 'Wonderland'

In this example, changes to deepCopy.name and deepCopy.address.city do not affect the original object because deepCopy is a completely independent copy.

Three ways to Create a Deep Copy:

  1. JSON Method: One common method to create a deep copy of an object is using JSON.stringify and JSON.parse. This is very simple and lightweight. However, this method has limitations, such as not supporting functions, undefined, Infinity, NaN, and circular references. This is because JSON can't copy non-serializable data (e.g., functions, undefined). So those things are stripped out.

    let deepCopy = JSON.parse(JSON.stringify(original));
  2. Lodash: The Lodash library provides a convenient _.cloneDeep method that handles more complex scenarios. Personally, I prefer this approach because it is cleaner and safer without building your own deep clone method. 

    const _ = require('lodash');
            let deepCopy = _.cloneDeep(original);
  3. structuredClone: The structuredClone method provides a built-in way to create a deep copy of an object, including handling circular references and other non-serializable data. This is a more modern approach that is supported in newer JavaScript environments.

    let deepCopy = structuredClone(original);

When to Use This Approach:

  • Use when you need a completely independent copy of the object or array
  • When you need all of the nested objects.

Using a deep copy ensures that changes to the copy do not affect the original. This is crucial when dealing with complex nested structures or ensuring full independence between copies. This is why I think of it as "Object Copy" - because I want a full replication of the object. Typically, I want to use the Lodash cloneDeep() method or structuredClone() for this. Even though it is the biggest performance hit, I only want to use it sparingly and be absolutely sure that my objects are not shared.

Choosing the Right Approach

  • Use Shallow Copy: When you need this.list to be independent of listData, preventing unintended side effects.
  • Use Direct Assignment: When you want this.list to reflect changes made to listData, maintaining a single source of truth.
Method Description Handles Nested Structures Copies Non-Serializable Data  Performance Impact When to Use

Reference

Direct Assignment

Assigns the object reference directly ๐Ÿšซ โœ…

Very Low

When you want changes in one object to reflect in another

Shallow

Spread Operator

Creates a shallow copy of the object using the spread operator ๐Ÿšซ โœ…

Low

When you need a quick shallow copy of an object

Shallow

Lodash _.clone

Creates a shallow copy using Lodash ๐Ÿšซ โœ…

Low

When using Lodash for better readability

Deep(ish)

JSON Methods

Creates a deep copy by serializing and parsing the object โœ… ๐Ÿšซ

Medium 
(limited by data types)

For simple objects with serializable data only

Deep

Lodash _.cloneDeep

Creates a deep copy using Lodash โœ… โœ…

High
(handles more scenarios)

When you need a reliable and thorough deep copy, and you have the Lodash library installed.

Deep

structuredClone

Creates a deep copy using native JS โœ… โœ…

High
(handles more scenarios)

When you need a reliable and thorough deep copy with native JS.

Understanding the difference between referencing and copying in JavaScript is fundamental for effective data management. Choosing the right approach can help you avoid common pitfalls and ensure your application behaves as expected. Whether you need object independence or a shared reference, you have a selection of different approaches to choose from.

By grasping these concepts, you can write cleaner, more predictable code, ultimately leading to more performant and maintainable applications. Using the appropriate method for each situation will help you manage your code.