In JavaScript, Primitive data types (numbers, strings, etc) are immutable but when it comes to objects and arrays they are mutable, please do not think that if you declare your objects and arrays with const
it will be constant
which is unchangeable:
const obj = {
a: "apple"
}
const updatedObj = obj
updatedObj.a = "banana"
console.log(obj.a) // "banana'
As you can see when we update updatedObj
's value, it updates the original object variable obj
as well. The reason behind it, objects copy by reference
, which means when we do const updatedObj = obj
updatedObj
is referencing/pointing to obj
's memory address, so if we update the updatedObj
we update obj
because they are pointing the same value. But in the case of primitive data types (numbers, strings, boolean, undefined, etc) is the opposite.
All primitives are immutable, i.e., they cannot be altered. It is important not to confuse a primitive itself with a variable assigned a primitive value. The variable may be reassigned a new value, but the existing value can not be changed in the ways that objects, arrays, and functions can be altered. ~ MDN
Here we can see examples where strings and numbers are not changing.
const num = 39
let updatedNum = num
updatedNum = 45
console.log(num) // 39
const str = "lion"
let updatedStr = str
updatedStr = "tiger"
console.log(str) // "lion"
Why do we care about immutability? If JavaScript was built this way then there must be a reason. Yes, it's because JavaScript is a multiparadigm language you can use it as OOP, you can use it as FP (functional programming).
Functional programming embraces immutability and heavily practices persistent data structure. And new libraries like React and Redux take the advantages of immutability, like in Redux, store
is one giant, plain JS object, immutable one and this gave the possibility for redux time travel
where you can see the previous states/changes or in React you can check the previous values of your local states, they all come from the object immutability.
Here is a simple example of creating an immutable object in JS:
const obj = {
a: "apple"
}
const updatedObj = Object.assign({}, obj)
updatedObj.a = "banana"
console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
Now we do not mutate our original object obj
.
We will have more practical examples on "How not to mutate your objects and arrays" in the next articles.
You might ask a question 🙋♂️ , "Wait if we do not mutate our object value? Then that must be lots of memory consumptions? " ~ You are not wrong!
That's where comes structural sharing
, you don't want to deep copy
the object but shallow copy
it. Just like git
does not copy your whole versions of your code but shares the files that are not changed with the previous commit.
Object.assign()
method does shallow copying
. But there is one downside to it, if you have nested object properties, they will not be immutable.
const obj = {
a: "apple",
b: {
c: "lemon"
}
}
const updatedObj = Object.assign({}, obj)
updatedObj.a = "mango"
updatedObj.b.c = "banana"
console.log(obj.a) // "apple"
console.log(obj.b.c) // "banana"
b: { c: "lemon" }
is not immutable here as it's nested property, we will see examples of how to make objects and arrays immutable including nested (complex structures) ones as well.
So shallow copying
will not take lots of memory consumptions.
Immutable Objects
- Using
Object.assign()
let obj = {
a: "apple"
}
let updatedObj = Object.assign({}, obj)
updatedObj.a = "banana"
console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
- Using
Object Spread Operators
:
let obj = {
a: "apple"
}
let updatedObj = { ...obj }
updatedObj.a = "banana"
console.log(obj.a) // "apple"
console.log(updatedObj.a) // "banana"
Spread Operators
are new ES6 syntax, similar to Object.assign()
method, it does shallow copying.
For complex data structure:
let obj = {
a: "apple",
b: {
c: "lemon"
}
}
let updatedObj = {...obj, b: { ...obj.b } };
updatedObj.a = "banana"
updatedObj.b.c = "peach"
console.log(obj.a) // "apple"
console.log(obj.b.c) // "lemon"
console.log(updatedObj.a) // "banana"
console.log(updatedObj.b.c) // "peach"
If you have nested object properties let updatedObj = {...obj, b: { ...obj.b } };
you can do nested spread with the property name.
Immutable Array
1.Array Spread Operators
let arr = [1, 2, 3, 4]
let updatedArr = [...arr]
updatedArr[2] = 5
console.log(arr[2])// 3
console.log(updatedArr[2])// 5
Array spread operators are the same as object spread operator, actually they are spread operators learn more here.
2.Using slice()
method:
let arr = [1, 2, 3, 4]
let updatedArr = arr.slice(0, arr.length);
updatedArr[2] = 5
console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
slice()
cuts the array from the index (first argument) until the index you want (second argument), but it won't affect the original array. There is splice()
array method, it's the opposite of slice()
it changes the content of the original array learn more on slice here, learn more on splice.
3.Using map()
, filter()
:
let arr = [1, 2, 3, 4]
let updatedArr = arr.map(function(value, index, arr){
return value;
});
updatedArr[2] = 5
console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
map()
returns a new array, takes a callback function as an argument and calls it on every element of the original array. Callback function takes value
(current iterated value), index
(current index), array
(original array) arguments, all of them are optional learn more here.
filter()
let arr = [1, 2, 3, 4]
let updatedArr = arr.filter(function(value, index, arr){
return value;
});
updatedArr[2] = 5
console.log(arr[2]) // 3
console.log(updatedArr[2]) // 5
console.log(updatedArr) // [1, 2, 5, 4]
filter()
and map()
works the same way learn more here.
They both return a new array.
map()
returns a new array of elements where you have applied some function on the element so that it changes the element.filter()
returns a new array of the elements of the original array (with no change to the elements).filter()
will only return elements where the function you specify returns a value of true for each element passed to the function.
There is one more method for array reduce()
, it will not return new array, but it will do immutable operations on an original array.
let arr = [1, 2, 3, 4];
// 1 + 2 + 3 + 4
const reducer = (accumulator, currentValue) => accumulator + currentValue;
let updatedArr = arr.reduce(reducer)
console.log(updatedArr) // 10
reduce()
could be confusing at the beginning, but I will try to explain as simply as possible. Let's look at the example below:
let sum = 0;
let i = 0;
while (i<arr.length){
sum+=arr[i]; // 1 + 2 + 3 + 4
i++;
}
console.log(sum) // 10
It just a loop that sums all the values of an array. We are trying to do the same thing with reduce()
.
reduce()
takes reducer
callback which is a function takes 4 arguments, accumulator
, currentValue
, currentIndex
, originalArray
. Accumulator saves the value which is returned from last iteration, just like sum
variable in our loop example, current value is arr[i]
. That's reduce
learn more here.
I hope 🤞 it all makes sense.
Extra Resources:
This answer here gives a great explanation on "why is immutability important?",
Top comments (0)