Overview
Javascript has 6 primitive data types:
string
,number
,bigint
,boolean
,undefined
,symbol
. Althoughnull
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 fromnull
by thePrototypal Chain
and thereforetypeof
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 likeArray
,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.
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);