DEV Community

Cover image for JavaScript Tricky Outputs: Unveiling the Mysteries
Jaimal Dullat
Jaimal Dullat

Posted on • Originally published at Medium

JavaScript Tricky Outputs: Unveiling the Mysteries

Introduction

Welcome, fellow readers! Today, we’re diving into the tricky side of Javascript. Before we begin, let’s clear up some things. Javascript is a programming language used to create interactive and dynamic web content. Tricky outputs refer to nuances in the language that may catch you off guard if you’re not familiar with them. It’s crucial to understand these outputs because they can affect the functionality and reliability of your code.

In this blog, we will explore some of the most common JavaScript tricky outputs, along with explanations and examples to help you understand and navigate through these challenges.


1. NaN — Not a Number

One of the most notorious tricky outputs in JavaScript is NaN, which stands for “Not a Number.” It is often encountered when performing mathematical operations that result in an undefined or incorrect value.

a. Invalid Mathematical Operations

When you perform mathematical operations that involve non-numeric values or undefined variables, you get NaN. For example:

console.log(0 / 0); // Output: NaN
console.log("Hello" - 10); // Output: NaN
console.log("Hello" * 10); // Output: NaN
Enter fullscreen mode Exit fullscreen mode

b. parseInt and parseFloat:

The parseInt and parseFloat functions can return NaN if the string they are parsing doesn't represent a valid number:

let num = parseInt("abc"); // NaN

Enter fullscreen mode Exit fullscreen mode

c. Math Functions:

Certain math functions may return NaN for invalid inputs:

let result = Math.sqrt(-1); // NaN

Enter fullscreen mode Exit fullscreen mode

d. Comparing NaN:

NaN is not equal to any value, including itself. So, comparing NaN with anything, even another NaN, results in false:

let nan1 = NaN;
let nan2 = NaN;
console.log(nan1 === nan2); // false
Enter fullscreen mode Exit fullscreen mode

e. Object Conversion:

Attempting to convert an object that doesn’t have a meaningful numeric representation to a number can result in NaN:

let obj = { key: "value" };
let num = Number(obj); // NaN
Enter fullscreen mode Exit fullscreen mode

f. Undefined Variables:

Using a variable that is not defined results in NaN:

let undefinedVar;
let result = undefinedVar * 10; // NaN
Enter fullscreen mode Exit fullscreen mode

g. NaN as a Property Value:

You can set a property of an object to NaN explicitly:

let obj = { prop: NaN };

Enter fullscreen mode Exit fullscreen mode

Remember that NaN is used to indicate that something went wrong with a numeric operation, so it’s essential to handle it properly in your code, especially when it might occur unexpectedly. You can use the isNaN() function or the Number.isNaN() method to check for NaN values and handle them accordingly.

console.log(isNaN(NaN)); // Output: true
console.log(isNaN(10)); // Output: false
Enter fullscreen mode Exit fullscreen mode

2. Type Coercion

Type coercion in JavaScript is the process of converting one data type into another implicitly or explicitly. It can sometimes lead to unexpected behavior if you’re not aware of how JavaScript handles type conversions. Let’s explore some examples of type coercion and discuss potential solutions.

Example 1:

Implicit Type Coercion

let num = 5;
let str = "10";
let result = num + str; // JavaScript implicitly converts num to a string and performs string concatenation

console.log(result); // "510"
Enter fullscreen mode Exit fullscreen mode

In this example, JavaScript implicitly converts the num variable to a string and then concatenates it with the string in the str variable. To avoid implicit coercion, you can use explicit type conversion (casting) to ensure you get the desired behavior:

Solution 1:

Explicit Type Conversion

let num = 5;
let str = "10";
let result = num + Number(str); // Explicitly convert str to a number

console.log(result); // 15
Enter fullscreen mode Exit fullscreen mode

Example 2:

Loose Equality Operator (==)

let num = 5;
let str = "5";
if (num == str) {
  console.log("Equal");
} else {
  console.log("Not Equal");
}

// Output: Equal
Enter fullscreen mode Exit fullscreen mode

In this example, the loose equality operator (==) performs type coercion and considers the values as equal even though one is a number and the other is a string.

Solution 2:

Strict Equality Operator (===)

let num = 5;
let str = "5";
if (num === str) {
  console.log("Equal");
} else {
  console.log("Not Equal");
}

// Output: Not Equal
Enter fullscreen mode Exit fullscreen mode

Using the strict equality operator (===) ensures that both the value and the type must be the same for the condition to be true. In this case, it would correctly identify them as not equal.

Example 3:

Truthy and Falsy Values

let value = "Hello";
if (value) {
  console.log("Truthy");
} else {
  console.log("Falsy");
}

// Output: Truthy
Enter fullscreen mode Exit fullscreen mode

JavaScript treats certain values as “truthy” (values that evaluate to true) and others as "falsy" (values that evaluate to false). This can lead to unexpected behavior when working with conditional statements.

Solution 3:

Use Explicit Comparisons

let value = "Hello";
if (value !== undefined && value !== null && value !== false && value !== 0 && value !== "") {
  console.log("Truthy");
}
Enter fullscreen mode Exit fullscreen mode

3. Hoisting

Hoisting in JavaScript can be tricky to understand, especially when it comes to variable and function declarations. Let’s walk through an example step by step:

Consider the following JavaScript code:

console.log(x); // Outout: undefined , Step 1: This line is executed, but 'x' is not defined yet.
var x = 5;     // Step 2: Variable 'x' is declared and initialized to 5.
console.log(x); // Output: 5, Step 3: This line now logs the value of 'x', which is 5.
Enter fullscreen mode Exit fullscreen mode

Now, let’s break down what happens step by step:

Step 1:

  • The code begins executing, and it encounters the console.log(x) statement.

  • At this point, x is not defined (no value assigned), but it doesn't result in an error. Instead, JavaScript recognizes x as a variable declaration due to hoisting.

  • The variable x is hoisted to the top of its containing scope (in this case, the global scope), but it's not yet assigned a value. So, console.log(x) logs undefined.

Step 2:

  • Now, we have var x = 5;. Here, x is declared and initialized to 5.

  • The assignment x = 5 happens at this point.

  • At the same time, JavaScript has already hoisted the variable declaration var x to the top of the scope, so it knows about the variable.

Step 3:

  • Finally, we encounter console.log(x) again.

  • Now, x has a value of 5 because it was assigned that value in Step 2.

  • The line logs 5 to the console.


4. The ‘this’ Keyword

The this keyword in JavaScript can be tricky to work with because its value depends on how a function is called. It doesn't always refer to the object or context you might expect. Here are some tricky situations involving this in JavaScript along with their solutions:

Example 1:

Global Context

console.log(this); // refers to the global object (e.g., window in a browser)
Enter fullscreen mode Exit fullscreen mode

Solution 1:

There’s no direct way to change the behavior of this in the global context. It will always refer to the global object. You can avoid relying on it in global scope or use strict mode to prevent accidental global variable creation.

Examples 2:

Regular Functions vs. Arrow Functions

const person = {
  name: 'John',
  regularFunc: function () {
    console.log(`Hello, my name is ${this.name}`); // refers to the 'person' object
  },
  arrowFunc: () => {
    console.log(`Hello, my name is ${this.name}`); // refers to the enclosing scope (likely the global object)
  },
};

person.regularFun(); // Output: Hello, my name is John
person.arrowFun(); // Output: Hello, my name is undefined
Enter fullscreen mode Exit fullscreen mode

In the above example, the regularFunc method is a regular function defined within the person object. When person.regularFunc() is called, this inside the regularFunc function refers to the person object. Therefore, this.name accesses the name property of the person object, resulting in the output “Hello, my name is John”.

The arrowFunmethod is an arrow function defined within the person object. When person.arrowFun() is called, this inside the arrow function does not refer to the person object. Instead, it inherits the this value from the surrounding lexical scope, which is the global object. As a result, this.name does not exist in the global object, leading to the output “Hello, my name is undefined”.

Solution 2:

Use regular functions when you need access to the object’s context (this). Arrow functions do not have their own this, and they inherit it from the enclosing scope.

Example 3:

Event Handlers

const button = document.querySelector('button');
button.addEventListener('click', function () {
  console.log(this); // refers to the button element
});
Enter fullscreen mode Exit fullscreen mode

Solution 3:

In event handlers, this often refers to the DOM element that triggered the event. If you need to access the outer context, store it in a variable before the event handler.

const button = document.querySelector('button');
const self = this; // Store the context
button.addEventListener('click', function () {
console.log(self); // refers to the outer context
});

Example 4:

Constructor Functions

function Person(name) {
  this.name = name;
}
const person = new Person('Alice');
console.log(person.name); // Alice
Enter fullscreen mode Exit fullscreen mode

Constructor functions create new objects and set this to refer to the newly created object. This behavior is not an issue unless you forget to use the new keyword.

Example 5:

Method Chaining

const calculator = {
  value: 0,
  add(num) {
    this.value += num;
    return this;
  },
  subtract(num) {
    this.value -= num;
      return this;
  },
};

const result = calculator.add(5).subtract(3).value; // What's the result?
// Output: 2
Enter fullscreen mode Exit fullscreen mode

This is a common pattern for method chaining. Each method returns this, allowing you to chain multiple method calls together.


5. Closures

Closures are a powerful concept in JavaScript but can also lead to unexpected outputs if not properly understood. A closure is created every time a function is created, at function creation time.

Consider the following example:

function outer() {
  var x = 10;

  function inner() {
     console.log(x);
  }

  return inner;
}

var newFunction = outer(); // Outer finished execution and returned inner function
newFunction(); // Output: 10
Enter fullscreen mode Exit fullscreen mode

In the above code, inner() has access to the x variable, even after outer() has finished executing.

What we're actually doing is invoking the inner function, which was returned by the outer function. And even though outer has long since completed its execution, inner can still access the variable x (thanks to the closure), and it prints out 10. Understanding closures is crucial to avoid unexpected behaviors when dealing with nested functions.

Conclusion

So there you have it, tricky outputs in Javascript. We’ve covered NaN — Not a Number, type coercion, hoisting, this keyword, and closures. It’s a lot to take in, we know. But don’t worry, understanding these concepts is crucial for any Javascript developer looking to write clean and efficient code.

It’s easy to fall into common misconceptions or make mistakes with these tricky outputs, but with a little bit of practice, you’ll be able to utilize them to your advantage. So remember, always test your code thoroughly and be mindful of how these outputs can affect your code.

🔗 Connect with me on:

A Big Thank You! 🌟

  • Readers: Grateful for every click and read.
  • Engagers: Claps, comments, shares — you’re the best!
  • Followers: Honored by your continuous support.
  • Silent Admirers: Your presence doesn’t go unnoticed.
  • Constructive Critics: Your insights help us improve. 💌 Stay connected and thanks for being part of our journey.

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy 🎖️

A closure is created when a function is defined within another function and has access to its parent function’s variables.

Unfortunately, this isn't really correct. A closure is created EVERY time a function is created, regardless of whether it was inside another function. You also imply that a closure is a function - which is also incorrect.

Collapse
 
jaimaldullat profile image
Jaimal Dullat

Thanks @jonrandy ... I will update.