In JavaScript, like any other program languages, stores things in variables, which can be changed on the fly. This may be a problem because we may change things accidentally that is shared. Having lots of code share the same mutable state is hard to trace. It makes debugging and read the code hard.
For example, if we change the same array in different function as follows:
let arr = [];
const foo = () => {
arr = [1, 2, 3];
}
const bar = () => {
arr = [4, 5, 6];
}
Then the value of arr
changes depending on whether foo
or bar
is called last. If foo
is called then arr
is [1, 2, 3]
. On the other hand, if bar
is called then arr
is [4, 5, 6]
.
This is a problem because as the code gets more complex, then the more function calls there are. If lots of functions are doing things like this, then tracing the value is hard and debugging is confusing.
Also, it’s hard to read how the logic flows as functions are called with these side effects.
There’re a few ways to avoid this situation. There’s the const
keyword to prevent reassignment. Also, we can copy objects to prevent the original from being modified.
Using Const for Constants
If we want to share constants between different parts of our code, then we can use the const
keyword to declare constants. This prevents them from being modified.
For example, if we write:
const arr = [];
const foo = () => {
arr = [1, 2, 3];
}
const bar = () => {
arr = [4, 5, 6];
}
Then whenever foo
or bar
are called, we’ll get an error.
Shallow Copying Data
We can also prevent the original piece of data from being changed while we manipulate data in our functions by making a copy of the original.
There’re 2 ways to copy data. One is to do a shallow copy, where we copy the top-level entries of an object or array.
If we have nested arrays or objects, then we have to do a deep copy, where we copy all the levels of an object or array.
In this article, we’ll look at how to shallow copy data.
To make a shallow copy, we can use the spread operator. It works for both objects an arrays.
For example, we can write the following for objects:
let obj = {
a: 1,
b: 2,
c: 3
};
let objCopy= {
...obj
};
And we can write the following for arrays:
let arr = [1, 2, 3];
let arrCopy = [...arr];
There’re a few limitations with using the spread operator. First, the prototype isn’t copied, so if we have things that inherit from some prototype like we have in the following code:
let obj = Object.create({
foo: 1
})
obj.a = 1;
obj.a = 2;
let objCopy = {
...obj
};
console.log(obj.__proto__);
console.log(objCopy.__proto__);
We see that the first console.log
output is completely different from the second. obj
‘s prorotype is {foo: 1}
which we explicitly set. However, objCopy
‘s prototype is Object.prototype
, which is completely different.
Special objects like regular expressions also special internal slots that aren’t copied.
Also, we can see from the log output above that inherited values aren’t copied with the spread operator.
In addition, only enumerable properties are copied. This means for instance, if we copy an array to an object with the spread operator as follows:
let arr = [1, 2];
let obj = {
...arr
};
console.log(arr);
console.log(obj);
We missing the length
property from the second console.log
‘s output.
Finally, getters, setters and property descriptors also aren’t copied over. For example, if we write:
let obj = Object.create({
foo: 1
})
obj.a = 1;
Object.defineProperty(obj, 'b', {
value: 2,
writable: false,
enumerable: true
})
console.log(Object.getOwnPropertyDescriptors(obj))
console.log(Object.getOwnPropertyDescriptors(objCopy))
Then we that the property descriptors of obj
and objCopy
are different.
We can get around some of these issues. We can copy the prototype of the original object into the new object as follows:
let obj = Object.create({
foo: 1
})
obj.a = 1;
obj.a = 2;
let objCopy = {
__proto__: Object.getPrototypeOf(obj),
...obj
};
console.log(obj.__proto__);
console.log(objCopy.__proto__);
Then we get that both obj
and objCopy
have the same prototype in the console.log
statements above.
We can copy over the value along with other property descriptors by writing the following code:
let obj = Object.create({
foo: 1
})
obj.a = 1;
Object.defineProperty(obj, 'b', {
value: 2,
writable: false,
enumerable: true
})
let objCopy = Object.defineProperties({}, Object.getOwnPropertyDescriptors(obj))
console.log(Object.getOwnPropertyDescriptors(obj))
console.log(Object.getOwnPropertyDescriptors(objCopy))
The property descriptor object includes the value of a property, so we can define all the properties with the defineProperties
method and pass in the property descriptors with the getOwnPropertyDescriptors
called with obj
passed in to get obj
‘s property descriptors.
We should see that property b
has writable
set to false
in the property descriptor of both obj
and objCopy
.
Preventing shared mutable state is a problem in JavaScript. We want to avoid this to prevent mutating shared data, which makes tracing code and debugging tough.
Copying objects in JavaScript precisely is tricky. The spread operator doesn’t do a thorough copy of an object. The property descriptors, getters and settings, and prototype aren’t copied over. Most of these issues can be solved by copying them over manually as we did with the object’s prototype and property descriptors.
For constants, we use const
to prevent accidental reassignment.