Value VS Reference in Javascript

Overview

  • Javascript has 6 primitive data types: string, number, bigint, boolean, undefined, symbol. Although null is also considered as a primitive data type because of it's behavior, But in certain cases, null is not as "primitive" as it first seems! since every object is derived from null by the Prototypal Chain and therefore typeof operator returns object for it.

  • The primitive data types are copied by value.

  • Javascript also provides another data structure like Object, which itself is used for creating other non-primitive data-types like Array, Function, Map, Set, WeakMap, WeakSet, Date.

  • These non-primitive data types are copied by reference.

Primitive Data Types Examples

Let's take the examples of copying primitive data types. Here we can see that the values are copied as it is to other variables.

let a1 = "Javascript";
let b1 = 10;
let a2 = a1;
let b2 = b1;
console.log(a1, b1, a2, b2); 
// Javascript 10 Javascript 10

Now, if we assign something else to the previously declared a2 and b2 variables, we can see that the value stored inside a1 and b1 didn't get impacted.

let a1 = "Javascript";
let b1 = 10;

let a2 = a1;
let b2 = b1;

a2 = "Java";
b2 = 20;
console.log(a1, b1, a2, b2); 
// Javascript 10 Java 20

Non-Primitive Data Types Examples

Now suppose we've a non-primitive data type and we copy it to another variable.

let arr1 = ["1", "2", "3", "4"];
let arr2 = arr1;
console.log(arr1, arr2); 
// ["1", "2", "3", "4"]
// ["1", "2", "3", "4"]

But now if we make some change to arr2.

arr2[2] = "5";
console.log(arr1, arr2);
// ["1", "2", "5", "4"]
// ["1", "2", "5", "4"]

We can see that the change made to the copied array arr2 also reflects in the original array arr1. So what happens when we did arr2 = arr1 was, we assigned the reference of the value stored inside arr1 to arr2. And this is the case with all non-primitive data types.


So what can be done if suppose we want to copy a non-primitive data type, say array for example.

let arr = ["1", "2", "3", "4"];
// Option-1: Using Array.prototype.slice() method. [Shallow Copy]
let arrCopy1 = arr.slice();
// Option-2: Using Array.prototype.concat() method. [Shallow Copy]
let arrCopy2 = [].concat(arr);
// Option-3: Using es6 spread operator. [Shallow Copy]
let arrCopy3 = [...arr];
// Option-4: Using Array.from() method [Shallow Copy]
let arrCopy4 = Array.from(arr);

So now if we change anything inside these new copied arrays, the original values inside arr won't change. For shallow copying of Objects use Object.assign()

let car = {"brand": "BMW", "wheels": 4};
let bike = Object.assign({}, car, {"wheels":2, "safety":3});
console.log(car, bike);
// {brand: "BMW", wheels: 4} {brand: "BMW", wheels: 2, safety: 3}

Shallow VS Deep Copy (Array)

But a thing to remember here is that all these techniques perform shallow copy instead of a deep copy, i.e. If the array is nested or multi-dimensional or contains objects, and if we change anything inside those it won't work. Let me explain with an example: Here I'm taking Array.prototype.slice() for copying but any of the others can also be used.

let obj1 = {"name":"shivaansh"};
let obj2 = {"name":"agarwal"};
let arr = [obj1, obj2];
let arrCopy1 = arr.slice();
arrCopy1[0].age = 22;
console.log(arr, arrCopy1);
/*
[{"name":"shivaansh", "age":22}, {"name":"agarwal"}]
[{"name":"shivaansh", "age":22}, {"name":"agarwal"}]
*/

As we can see here in case of deep copy the above technique fails.

So to avoid this some developers usually prefer using the JSON methods.

let obj1 = {"name":"shivaansh"};
let obj2 = {"name":"agarwal"};
let arr = [obj1, obj2];
let arrCopy1 = JSON.parse(JSON.stringify(arr));
arrCopy1[0].age = 22;
console.log(arr, arrCopy1);
/*
[{"name":"shivaansh"}, {"name":"agarwal"}]
[{"name":"shivaansh", "age":22}, {"name":"agarwal"}]
*/


But as pointed out by Samantha Ming in her blog, even JSON technique might fail as it'll not work with values not compatible with JSON like suppose if we've a function being assigned to an object property inside an array. Alt Text

Also, consider the following example,

function nestedCopy(array) {
    return JSON.parse(JSON.stringify(array));
}

// undefined are converted to nulls
nestedCopy([1, undefined, 2]) // -> [1, null, 2]

// DOM nodes are converted to empty objects
nestedCopy([document.body, document.querySelector('p')]) // -> [{}, {}]

// JS dates are converted to strings
nestedCopy([new Date()]) // -> ["2019-03-04T10:09:00.419Z"]

deepClone by lodash or custom function

  • JSON.stringify/parse only work with Number and String and Object literal without function or Symbol properties.
  • deepClone works with all types, function and Symbol are copied by reference.

Example of Lodash Solution by Alfredo Salzillo,

const lodashClonedeep = require("lodash.clonedeep");

const arrOfFunction = [() => 2, {
    test: () => 3,
}, Symbol('4')];

// deepClone copy by refence function and Symbol
console.log(lodashClonedeep(arrOfFunction));
// JSON replace function with null and function in object with undefined
console.log(JSON.parse(JSON.stringify(arrOfFunction)));

// function and symbol are copied by reference in deepClone
console.log(lodashClonedeep(arrOfFunction)[0] === lodashClonedeep(arrOfFunction)[0]);
console.log(lodashClonedeep(arrOfFunction)[2] === lodashClonedeep(arrOfFunction)[2]);

Example of Recursive function Solution by Tareq Al-Zubaidi

const clone = (items) => items.map(item => Array.isArray(item) ? clone(item) : item);

References:

  1. educative.io/courses/step-up-your-js-a-comp..
  2. stackoverflow.com/questions/6605640/javascr..
  3. freecodecamp.org/news/understanding-by-refe..
  4. javascript.info/object-copy
  5. dev.to/samanthaming/how-to-deep-clone-an-ar..
  6. Javascript30 Course by WesBros