Referential equality: Where variables check-in and rarely check-out.
Subaash
Posted on January 19, 2024
Hi fellow devs, we are here to dive deep into the heart of JavaScript's strict equality operator ('===') and figure out why some variables behave like identical twins while others behave like awkward strangers in an alumni reunion.
Before knowing about the equality operator, we should understand what are primitive values and non-primitive values in JavaScript.
Primitive values
Primitives are basic, fundamental data types that serve as the building blocks to build a complex data type. The following are some primitives in JavaScript.
String
Number
Boolean
Undefined
Null
Non-Primitives
Any data type other than the primitives are Non-Primitives. Some of them are,
Array
Object
Function
RegExp
Set
Map
Promise
Examples
1) Primitives and the effect of Equality
Whenever we strictly equate('===') two primitives of a similar data type, they always yield true
.
let fruit_1 = 'apple'
let fruit_2 = 'apple'
console.log(fruit_1 === fruit_2) //Output: true
2) Non-Primitives and the effects of Referential Equality
When two non-primitives of the same data type are strict-equated, the results will be surprising. Look at the following snippet where two arrays with similar data types and similar items are equated.
let basket_1 = [ 'apple', 'banana' ]
let basket_2 = [ 'apple', 'banana' ]
console.log(basket_1 === basket_2) //Output: false
But, when we check the equality criteria of basket_3
and basket_1
, it returns true
let basket_1 = [ 'apple', 'banana' ]
let basket_2 = [ 'apple', 'banana' ]
let basket_3 = basket_1
console.log(basket_3 === basket_1) //Output: true;
Let's try one more thing.
When we push another item into basket_3
, it should not affect basket_1
. But it does.
let basket_1 = [ 'apple', 'banana' ]
let basket_2 = [ 'apple', 'banana' ]
let basket_3 = basket_1
basket_3.push('orange')
console.log(basket_1) //Output: [ 'apple', 'banana', 'orange' ]
console.log(basket_3) //Output: [ 'apple', 'banana', 'orange' ]
But why is this happening?
This is where we should know about Referential equality.
Referential Equality
This seems to be a Jargon, doesn't it? Actually No.
Referential Equality is like asking, "Are these two non-primitive things pointing to the same spot in the computer's memory ?" If yes, they are referentially equal(i.e., true
) otherwise, they aren't(i.e., false
). As simple as that.
Take a look at the following representation to get it better.
Even though the basket_1
and basket_2
arrays have similar items, they occupy different locations in memory. When we use strict equality (===) to compare them, JavaScript checks whether they refer to the same location in memory, and since they are on distinct memory locations, the result is false
.
But, things changed when we declared basket_3
and assigned it to basket_1
By assigning let basket_3 = basket_1
, we forced the variable basket_3
to point to the exact memory address as basket_1
. So the result was true when we did console.log(basket_3===basket_1)
.
As basket_3
and basket_1
point the same memory location, changes made through one of the variables will reflect in the other. Therefore, when we push the item 'orange' only into basket_3
, we observe that the item 'orange' is now present in both arrays
However, for primitive types like strings, numbers etc., we are more interested in comparing their values rather than their references. JavaScript decides whether to compare a variable by the value or by its reference based on its primitive and non-primitive nature.
How to work with Non-primitives?
How do we work with them if they keep sharing their addresses? JavaScript has nothing to do with this. Even some of the libraries like React, utilize this uniqueness of the non-primitives to control the component renders.
But, we can prevent them from sharing memory locations or memory addresses.
If the array is on the top level (i.e., no nesting within arrays) we can shallow copy the existing array into the other.
Create a shallow copy using the spread operator (...)
let fruit_1 = ['apple'];
let fruit_2 = ['apple'];
let fruit_3 = [...fruit_1]
fruit_3.push('orange')
console.log(fruit_3) //Output: ['apple', 'orange']
console.log(fruit_1) //Output: ['apple']
Now you may get a question. What about the other non-primitives? Will this even work with JavaScript objects? The answer is, YES.
Let's take a complex example of an object whose values are nested deeper than the top level.
Wait, What? The shallow copy is supposed to copy the contents into a new memory address. But in the above case, making changes in the inventory
object changes the shop
object as well. So, this is the catch with the shallow copy. You cannot use shallow copy with deeply nested objects and arrays.
We can overcome this by using JSON.stringify()
and JSON.parse()
According to the above representation, changes made on inventory
object have no impact on the shop
object. And this is how we perform deep copy of nested objects.
If you are confused with JSON.parse() and JSON.stringify(), please refer to the official documentation available at the end of this article.
Using JSON methods has its drawbacks. They can be rectified by using appropriate methods and techniques. Given the article's length, let's wrap up our discussion here and I look forward on to delving another article, discussing the alternatives to the JSON methods. Thank you for reading and have a nice day.
Here are the official MDN docs on JSON.parse() and JSON.strigify() methods
Follow me on X, if you wish to see my progress in becoming a full-stack Java developer.
Posted on January 19, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 22, 2024