Tricky Parts of JavaScript I — Data Types vs Assignment & Equality Operators
In the world of programming, JavaScript, like any language, has its fair share of tricky aspects that can stump developers. Whether you’re a seasoned pro or just starting out, these intricacies are bound to cross your path as you build web applications or tinker with server-side logic using Node.js. In this article and the next, we’ll explore some of these tricky elements.
So, let’s dive right in.
1. Primitive vs. Reference Data Types and Assignment Behaviour
In JavaScript, data types are broadly categorised into two classes: primitive data types, often referred to as “value data types,” and reference data types. These two categories exhibit distinct behaviours when it comes to assignment.
Let’s delve into some examples to illuminate this concept:
// Example Set 1
let a = 5;
let b = a;
b = 10;
console.log(a); // Output: 5
console.log(b); // Output: 10
// Example Set 2
let str1 = "Hello";
let str2 = str1;
str2 += " World";
console.log(str1); // Output: "Hello"
console.log(str2); // Output: "Hello World"
// Example Set 3
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // Output: [1, 2, 3, 4]
console.log(arr2); // Output: [1, 2, 3, 4]
// Example Set 4
let obj1 = { name: "Ivan" };
let obj2 = obj1;
obj2.name = "Esther";
console.log(obj1.name); // Output: "Esther"
console.log(obj2.name); // Output: "Esther"
Example Sets 1 & 2 — Primitive Values (Number and String)
In these set of examples, we’re working with primitive values of the Number and String data types, which belong to the category of Primitive Data Types. JavaScript also includes other primitive data types, such as booleans, null, undefined, symbols, and BigInt.
Let’s look at three characteristics of Primitive Data types which are essential to understanding the behaviours in these two set examples:
Direct Storage: Primitive values are stored directly in memory. What this means is that when you assign a primitive value to a variable, that variable holds the actual value directly in memory.
Passed by Value: Primitive values are passed by value. What this means is that when you assign a primitive value to a another variable, a copy of the actual value is made. Modifying the copied value does not affect the original value.
Immutable: Once a primitive value is created, it cannot be changed. Any operation that appears to modify a primitive value actually creates a new value.
In the example set 1:
Variable
a
is assigned the primitive value5
anda
holds the value directly in memory.When
b
is assigned the value ofa
usingb = a
, a copy of the actual primitive value ina
(which is5
) is made and stored inb
due to the passed by value property.Subsequently, when
b
is reassigned the value of10
, this change does not propagate back toa
since each variable has its own independent copy of the value5
.console.log(a);
outputs5
because the value ofa
remains unaffected by the subsequent assignment tob
.console.log(b);
outputs10
becauseb
was assigned the value10
after initially holding the value ofa
.
Similarly, with strings in example set 2:
Variables
str1
andstr2
initially hold distinct copies of the primitive value"Hello"
.When you concatenate
" World"
tostr2
using the+=
operator, it creates a new string(due to the immutability property) with the combined value and assigns it tostr2
.Importantly, this operation does not modify the original string value in
str1
, and the value instr1
remains unchanged.
Example Sets 3 & 4— Reference Types(Arrays and Objects)
In these examples, we are dealing with Arrays and Objects which are Reference data types. Other examples are Functions and Classes. Here are some key characteristics of reference data types:
Stored by Reference: When you create a variable and assign a reference data type to that variable, the variable effectively holds a reference or memory address to the location in memory where the data is stored.
Passed by Reference: When you assign a reference data type to another variable, you are effectively passing or assigning the reference, not duplicating the data. As a result, any changes made through the new variable affects the original data.
Mutable: Reference data types are mutable, meaning that you can modify their content after they are created. Changes made to the data through one reference affect all references pointing to the same data.
In example set 3:
Variables
arr1
andarr2
hold a shared reference to the same array data.When
arr2
is modified by pushing the value 4 into it, this change directly affects the underlying data that botharr1
andarr2
point to.As a result, the data within the array is altered, and both
arr1
andarr2
yield the same updated array with the added value 4.
In example set 4:
Variables
obj1
andobj2
similarly share a reference to the same object data.The alteration of the
name
property inobj2
from "Ivan" to "Esther" impacts the underlying object that both variables reference.Consequently, the change is reflected in both
obj1
andobj2
, and they both display the updatedname
property value, which is "Esther".
2. Equality Operators
JavaScript has two types of equality operators: ==
(loose equality) and ===
(strict equality). These operators can be tricky, as they don’t always behave as one might expect, particularly when comparing different data types or values.
Let’s explore both and see how they can be tricky with some examples:
// Example Set 1
1 == '1'; // true (string '1' is coerced to a number for comparison)
0 == false; // true (coerces boolean to number for comparison)
"0" == false; // true (both are coerced to number 0)
null == undefined; // true (both are considered equal in ==)
// Example Set 2
1 === '1'; // false (number is not equal to string)
0 === false; // false (number is not equal to boolean)
"0" == false; // false (string is not equal to boolean)
null === undefined; // false (different data types)
To understand these behaviors, let’s dive deeper into each operator type:
Loose Equality (==):
The loose equality operator compares values while allowing type coercion, meaning it attempts to convert the values into a common type before making the comparison. This can lead to unexpected results, making it generally advisable to avoid its use.
In the first example set, the operands are coerced to the same data type before the comparison, hence returning true for all comparisons.
Strict Equality (===):
The strict equality operator, on the other hand, compares values for equality without performing type coercion. It checks not only the value but also the data type. This typically leads to more predictable results.
In the second example set, the operands aren’t coerced, and their data types are explicitly considered, leading to false results for all comparisons.
Note: Comparing Reference Data Types
When comparing reference data types, such as arrays and objects, JavaScript’s equality operators check if the references point to the same data in memory, not if their content is the same.
//Will Return False
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
arr1 == arr2; // false (arrays are compared by reference, not by content)
arr1 === arr2; // false (for the same reason as above)
const obj1 = { key: "value" };
const obj2 = { key: "value" };
obj1 == obj2; // false (objects are compared by reference, not by content)
obj1 === obj2; // false (for the same reason as above)
//Will Return True
const arr1 = [1, 2, 3];
const arr2 = arr1; // arr2 reference the same array as arr1
arr1 == arr2; // true (both variables reference the same array)
arr1 === arr2; // true (for the same reason as above)
const obj1 = { key: "value" };
const obj2 = obj1; // obj2 reference the same object as obj1
obj1 == obj2; // true (both variables reference the same object)
obj1 === obj2; // true (for the same reason as above)
For comparing the content of reference data types, you’ll usually need to iterate through their properties or elements to check for equality. Alternatively, you can serialize objects to JSON strings with JSON.stringify()
and compare those strings, or use libraries like lodash or Underscore.js, which offer the isEqual()
utility function for such comparisons.
Conclusion
In this article, we uncovered some intricacies of primitive and reference data types and their corresponding behaviors with respect to assignment operators. We also covered the differences in equality operators and how they can sometimes lead to surprising results.
But our journey is far from over. Stay tuned for the next article where we’ll dive into scope and hoisting, shedding light on how hoisting can sometimes lead to surprising behavior. Additionally, we’ll explore the immutability of variables created with const
and how it can affect your code.