Data Types in JS#
We know that in the JavaScript language, there are primitive data types and reference data types.
The variable name and value of primitive data types are stored in the stack memory. Each time a variable of a primitive data type is declared, a new space is allocated in the stack memory to store the variable name and value, and there is no impact between them.
However, it is different for reference types. The variable name and value of reference types are stored in the stack memory, that's correct. But the value stored is the address of the corresponding heap memory. The actual value is stored in the heap memory, and the stack memory only stores the variable name and the address information of the heap memory.
The above allocation method for reference data types seems to be fine, but there may be strange situations in actual use:
const obj = { a: 123, b: 456 }
const obj2 = obj
obj.a = 789
console.log(obj2.a)
Guess what will be printed?
Before understanding the relevant knowledge, you might instinctively think it will be 123
, but the actual answer is 789
, yes, you read it right, it's 789
. You can try it yourself by pressing F12
and going to the console.
But why is it 789
? Why not 123
?
Shallow Copy and Deep Copy#
The reason is that when we assign a reference data type variable using the assignment operator =
, due to the characteristics of the JavaScript language, the actual assignment is the address of the variable obj
. (That is, the object { a: 123, b: 456 }
is stored in the heap memory at that address)
You can understand it as: when you use =
, you are actually copying a "reference" of this object. obj
and obj2
share the same address in the heap memory (the stored address points to the same block of heap memory). It simply copies the reference address and still points to the same memory block. We call this method "assignment".
To solve the above problem, we need to "copy" the object. Depending on the degree of copying, there are two ways: shallow copy and deep copy.
Shallow copy creates a new object. If the property is a primitive type, it copies the value of the primitive type. If the property is a memory address (reference type), it copies the memory address. For deeply nested (more than one level) reference types, it still only copies the memory address.
In contrast, we call the copy method that can make the source object and the copied object "independent" of each other, and any changes to either object will not affect the other object as deep copy.
Shallow Copy#
The following situations belong to shallow copy (not completely copied, the new and old objects affect each other):
- Directly using the assignment operator
=
to assign a reference data type variable - Using array native methods such as
slice
,concat
, etc. to process two-dimensional or higher-dimensional arrays - Using the
Object.assign
method to copy an object, and the property values of the object include reference types - Using the spread operator
...
to copy data (objects or arrays) of two or more levels of reference types
Deep Copy#
The following situations belong to deep copy (completely copied, the new and old objects do not affect each other):
- Creating a new object and recursively copying each layer of primitive data types
JSON.parse(JSON.stringify(obj))
(not recommended)- Deep copy functions provided by libraries such as
jquery
,lodash
, etc.
Using JSON.parse(JSON.stringify(obj))
has many drawbacks:
- Properties with values of
undefined
, functions, orSymbol
will be ignored - Properties with values of
NaN
,Infinity
, or-Infinity
will be set tonull
- Built-in objects such as
Error
,RegExp
,Set
,Map
, etc. will be converted to empty objects Date
objects will be forcibly converted to strings- Properties on the prototype chain will also be copied
Implementing a Deep Copy Function#
To solve the strange behavior mentioned above, we need a function deepClone
to achieve the following effect:
const obj = { a: 123, b: 456 }
const obj2 = deepClone(obj)
obj.a = 789
console.log(obj2.a) // Output 123 instead of 789
First version of the code:
function deepClone(obj, useJSON = false) {
// Filter primitive data types
if (typeof obj !== 'object' || obj === null) return obj
// Deep copy implementation using JSON
if (useJSON) return JSON.parse(JSON.stringify(obj))
let _obj = Array.isArray(obj) ? [] : {}
for (const key in obj) {
// Do not copy properties on the prototype chain
if (!obj.hasOwnProperty(key)) return
if (typeof obj[key] === 'object' && obj[key] !== null) {
// If the property value is a reference type, recursively call the deepClone function for deep copy
_obj[key] = deepClone(obj[key])
} else {
// Copy primitive data types using the assignment operator
_obj[key] = obj[key]
}
}
return _obj
}
Let's test it:
const obj = { a: 123, b: { c: 456 } }
const obj2 = deepClone(obj)
obj.b.c = 789
console.log(obj2.b.c) // Output 456, test passed
The above implementation of deepClone
can indeed handle deep copy in general cases, but for some objects with built-in JavaScript objects as property values, the situation may not be so optimistic. Take a look at the example below:
const obj = {
a: new Date(),
b: { c: new Set([1]), d: new Map([['a', 1]]) },
e: { f: new Error('msg'), g: new RegExp('^obj$') },
h: e => console.log(e)
}
// Here we use the above deepClone function
const obj2 = deepClone(obj)
obj.b.c = e => console.log(e)
console.log(obj2)
// The output is as follows:
// {
// a: {},
// b: {
// c: {},
// d: {},
// },
// e: { f: {}, g: {} },
// h: [Function: h],
// };
As you can see, for built-in JavaScript objects such as Set
, Map
, Error
, RegExp
, Date
, etc., this function does not work as expected. It treats these built-in JavaScript objects as ordinary objects. Let's improve this function to restore JavaScript built-in objects.
Second version of the code (also the final version):
function deepClone(obj) {
// Define a function to get the type, Object.prototype.toString will return a detailed type description
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1)
}
// Filter primitive data types and built-in objects (Error, RegExp, Date, etc.)
if (getType(obj) !== 'Object' && getType(obj) !== 'Array') return obj
let _obj = Array.isArray(obj) ? [] : {}
for (const key in obj) {
// Do not copy properties on the prototype chain
if (!obj.hasOwnProperty(key)) continue
if (getType(obj) === 'Object' || getType(obj) === 'Array') {
// If the property value is an array or object, recursively call the deepClone function for deep copy
_obj[key] = deepClone(obj[key])
} else {
// Copy primitive data types and built-in JavaScript objects using the assignment operator
_obj[key] = obj[key]
}
}
return _obj
}
Let's test it again:
const obj = {
a: new Date(),
b: { c: new Set([1]), d: new Map([['a', 1]]) },
e: { f: new Error('msg'), g: new RegExp('^obj$') },
h: e => console.log(e)
}
const obj2 = deepClone(obj)
obj.b.c = e => console.log(e)
console.log(obj2)
// The output is as follows:
// {
// a: 2022-03-06T05:11:33.838Z,
// b: { c: Set(1) { 1 }, d: Map(1) { 'a' => 1 } },
// e: {
// f: Error: msg,
// g: /^obj$/
// },
// h: [Function: h]
// }
As you can see, the second version of the code handles the situation more scientifically and accurately. It uses Object.prototype.toString
to determine the type and restores the built-in JavaScript objects, making it more precise in copying object properties.
However, this version only implements the copying of ordinary objects. For complex types of objects (such as Map, Set, etc.), please use community solutions such as lodash or clone.
Updated on March 24, 2023#
In actual production environments, it is recommended to use rfdc, which is a community-developed npm package specifically designed for deep copying, with optimized performance.