DEV Community

Katherine Kelly
Katherine Kelly

Posted on

Copycat vs. Reference-cat

When dealing with data, developers oftentimes have to create copies in order to not mutate the dataset. In JavaScript, data types are either passed by value or passed by reference so there are different ways to make proper copies depending on what you're working with. And if you don't do it right, your copy won't end up being a copycat but turn out to be a reference-cat (terrible joke). Never heard of a reference-cat? Good, because it doesn't exist and your incorrectly copied variables shouldn't either.

simpsons copy gif

Primitive Data Types

Primitive data types are passed by value and are immutable. So if we change it, a new instance is created.

There are six primitive data types, checked by JavaScript's typeof operator:

  • Boolean: typeof instanceName === 'boolean’
  • String: typeof instanceName === 'string'
  • Number: typeof instanceName === 'number'
  • undefined: typeof instanceName === 'undefined'
  • BigInt: typeof instanceName === 'bigint'
  • Symbol: typeof instanceName === 'symbol'

Making a Copy of a Primitive Data Type

To make a copy, all you have to do is create a new variable and assign its value to the variable you want to copy.

let str1 = 'noodles'; 
str1; // 'noodles'
let str2 = str1; 
str2; // 'noodles'
str1 === str2; // true
str1 = 'pizza' // 'pizza'
str1 === str2; // false
str2; // 'noodles'

Above, I've created a new variable str2 and assigned its value to str1. JavaScript has allocated a separate memory spot for str2 so str1's value is reassigned. str2 is not affected because it is independent of str1.

Non-Primitive Data Types

Non-primitive data types, however, are passed by reference and are mutable. So if we change it, it can be difficult to keep track of and wacky things can happen if you're not careful. Non-primitive data types include Object, Array, Set, and Map. This means that arrays and objects assigned to a variable do not actually contain the other variable's values but rather point to the data type's reference in memory.

let obj1 = {1: 'noodles', 2: 'pizza'}; 
obj1; // {1: 'noodles', 2: 'pizza'}
let obj2 = obj1;
obj2; // {1: 'noodles', 2: 'pizza'}
obj1 === obj2; // true
obj1[3] = cake; 
obj1; // {1: 'noodles', 2: 'pizza', 3: 'cake'}
obj2; // {1: 'noodles', 2: 'pizza', 3: 'cake'}

On the surface, arr2 appears to get arr1's values, but it's only pointing to arr1's reference in memory. Any changes to made to arr1 will be reflected in arr2 and also vice versa as they are both pointing to the same reference.

Making a Copy of a Non-Primitive Data Type

There are a few different ways to make copies of objects in JavaScript. Depending on your needs, some of the methods will only shallow copy the object while others can support a deep copy.

Spread Operator

Using the spread operator will make a shallow copy of your object. It works great for all objects including arrays and objects.

const arr1 = ['noodles', 'pizza'];
const copyArr = [...arr1];
copyArr; // ['noodles', 'pizza']

const obj1 = {1: 'noodles', 2: 'pizza'}; 
const copyObj = {...obj1};
copyObj; // {1: 'noodles', 2: 'pizza'}

Object.assign()

Using Object.assign() will produce a shallow copy of your JavaScript object. Make sure to pass in an empty {} as the target argument so there is no mutation to be had.

//syntax
Object.assign(target, ...sources);
let obj1 = {1: 'noodles', 2: 'pizza'}; 
let copyObj = Object.assign({}, obj1};  // { '1': 'noodles', '2': 'pizza' }
obj1 === copyObj; // false;
obj1[3] = 'cake';
obj1;  // {1: 'noodles', 2: 'pizza', 3: 'cake'}
obj2; // {1: 'noodles', 2: 'pizza'}

Object.fromEntries(Object.entries())

Another shallow copy method for your JavaScript object is to use Object.fromEntries() in conjunction with Object.entries(). Introduced in ECMAScript 2019, Object.fromEntries() transforms a list of key-value pairs into an object. Using Object.entries() will turn the object you want to copy into key-value pairs, then using Object.fromEntries() will turn that into your very own object copy.

let obj1 = {1: 'noodles', 2: 'pizza'}; 
let copyObj = Object.fromEntries(Object.entries(obj1));
copyObj; // {1: 'noodles', 2: 'pizza'}

Array.from()

Similarly to above, you can use Array.from() to make shallow copies of arrays.

const arr1 = ['noodles', 'pizza'];
const copyArr = Array.from(arr1);
copyArr; // ['noodles', 'pizza']

JSON

For a deeper copy of your objects, you can use JSON to first stringify your object to JSON and then parse the string back into an object. However, using JSON to make a deep copy only works when the source object is JSON safe.

const obj1 = {1: 'noodles', 2: 'pizza', 3: 'cake'}; 
const copyObj = JSON.parse(JSON.stringify(obj1));
copyObj; // {1: 'noodles', 2: 'pizza', 3: 'cake'}

Alternatives Using Libraries

Though my copycat needs are typically met with one of the above methods, there are several external libraries out there that will deep copy your objects. These include Lodash's cloneDeep() and Underscore's clone().

Clone away with any one of these methods knowing your copies are copycats and not those pesky reference-cats!

Resources
Object.fromEntries()
3 Ways to Clone Objects in JavaScript
JavaScript Data Types and Data Structures

Discussion (0)