Tricky Parts of JavaScript I — Data Types vs Assignment & Equality Operators

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:

  1. 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.

  2. 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.

  3. 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 value 5 and a holds the value directly in memory.

  • When b is assigned the value of a using b = a, a copy of the actual primitive value in a (which is 5) is made and stored in b due to the passed by value property.

  • Subsequently, when b is reassigned the value of 10 , this change does not propagate back to a since each variable has its own independent copy of the value 5.

  • console.log(a); outputs 5 because the value of a remains unaffected by the subsequent assignment to b.

  • console.log(b); outputs 10 because b was assigned the value 10 after initially holding the value of a.

Similarly, with strings in example set 2:

  • Variables str1 and str2 initially hold distinct copies of the primitive value "Hello".

  • When you concatenate " World" to str2 using the += operator, it creates a new string(due to the immutability property) with the combined value and assigns it to str2.

  • Importantly, this operation does not modify the original string value in str1, and the value in str1 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:

  1. 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.

  2. 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.

  3. 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 and arr2 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 both arr1 and arr2 point to.

  • As a result, the data within the array is altered, and both arr1 and arr2 yield the same updated array with the added value 4.

In example set 4:

  • Variables obj1 and obj2 similarly share a reference to the same object data.

  • The alteration of the name property in obj2 from "Ivan" to "Esther" impacts the underlying object that both variables reference.

  • Consequently, the change is reflected in both obj1 and obj2, and they both display the updated name 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.