Skip to main content

Command Palette

Search for a command to run...

Advanced Concepts Every Developer Should Know – Scope, this, OOP, and Async Programming

A complete guide to understanding Scope, this, Object-Oriented Programming, and Asynchronous JavaScript

Published
9 min read
Advanced Concepts Every Developer Should Know – Scope, this, OOP, and Async Programming
S
I'm Shanu Tiwari, a passionate front-end software engineer. I'm driven by the power of technology to create innovative solutions and improve user experiences. Through my studies and personal projects, I have developed a strong foundation in programming languages such as Javascript and TypeScript, as well as a solid understanding of software development methodologies.

JavaScript is one of the most powerful and widely-used programming languages in the world. But to write clean, scalable, and bug-free code, you must understand its core concepts deeply.

In this blog, we’ll cover 4 fundamental topics every developer should master:

  1. Scope, Execution Context & Closures

  2. The this Keyword

  3. Object-Oriented JavaScript (OOP)

  4. Callbacks, Promises & Async/Await

1. Scope, Execution Context & Closures

Understanding scope is fundamental to writing effective JavaScript. Let's break down these interconnected concepts.

🔹Scope: Where Variables Live

Scope determines where variables can be accessed in your code. JavaScript has three main types of scope:

1. Global Scope

Variables declared outside any function or block have global scope and can be accessed from anywhere in your program.

javascript

var globalVar = "I'm global!";
let globalLet = "Me too!";

function someFunction() {
    console.log(globalVar); // ✅ Accessible
    console.log(globalLet); // ✅ Accessible
}

2. Function Scope

Variables declared inside a function are only accessible within that function.

javascript

function myFunction() {
    var functionScoped = "Only inside function";
    let alsoFunctionScoped = "Same here";

    console.log(functionScoped); // ✅ Works
}

console.log(functionScoped); // ❌ ReferenceError

3. Block Scope

Variables declared with let and const inside a block ({}) are only accessible within that block.

javascript

if (true) {
    let blockScoped = "Inside block";
    const alsoBlockScoped = "Me too";
    var notBlockScoped = "I escape!";

    console.log(blockScoped); // ✅ Works
}

console.log(blockScoped); // ❌ ReferenceError
console.log(notBlockScoped); // ✅ Works (var doesn't respect block scope)

🔹Execution Context: The Environment

Execution Context is the environment where JavaScript code is evaluated and executed. Every time a function is called, a new execution context is created.

The call stack manages these execution contexts, with the most recent one on top.

javascript

let name = "Global";

function outer() {
    let name = "Outer";

    function inner() {
        let name = "Inner";
        console.log(name); // "Inner"
    }

    inner();
    console.log(name); // "Outer"
}

outer();
console.log(name); // "Global"

Whenever JavaScript code runs, it creates an Execution Context (a container with variables, functions, and scope).

Two main types:

  • Global Execution Context (GEC) → Created when the program starts.

  • Function Execution Context (FEC) → Created whenever a function is called.

Each context has two phases:

  1. Creation Phase → Memory allocation (hoisting).

  2. Execution Phase → Code runs line by line.

🔹 Closures: Functions That Remember

A closure gives you access to an outer function's scope from an inner function. Even after the outer function has returned, the inner function remembers the outer function's variables.

javascript

function createCounter() {
    let count = 0;

    return function() {
        count++;
        return count;
    };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

// The inner function "closes over" the count variable

2. The this Keyword Demystified

The this keyword is one of JavaScript's most confusing features because its value changes depending on how a function is called, not where it's defined.

🔹this in Different Contexts

1. Global Scope

In the global scope, this refers to the global object (window in browsers, global in Node.js).

javascript

console.log(this); // Window object (in browser)

function globalFunction() {
    console.log(this); // Window object (in non-strict mode)
}

2. Function Context

In regular functions, this depends on how the function is called.

javascript

function regularFunction() {
    console.log(this);
}

regularFunction(); // Window object (non-strict mode) or undefined (strict mode)

const obj = {
    method: regularFunction
};

obj.method(); // obj (the object that called the method)

3. Method Context

When a function is called as a method of an object, this refers to that object.

javascript

const person = {
    name: "Alice",
    greet() {
        console.log(`Hello, I'm ${this.name}`);
    }
};

person.greet(); // "Hello, I'm Alice"

const greetFunction = person.greet;
greetFunction(); // "Hello, I'm undefined" (lost context!)

4. Event Handlers

In event handlers, this typically refers to the element that triggered the event.

javascript

button.addEventListener('click', function() {
    console.log(this); // The button element
    this.style.backgroundColor = 'red';
});

5. Class Context

In classes, this refers to the instance of the class.

javascript

class Car {
    constructor(brand) {
        this.brand = brand;
    }

    startEngine() {
        console.log(`${this.brand} engine started!`);
    }
}

const myCar = new Car("Toyota");
myCar.startEngine(); // "Toyota engine started!"

🔹 Arrow Functions & Lexical this

Arrow functions don't have their own this binding. They inherit this from the enclosing scope.

javascript

const obj = {
    name: "Bob",
    regularMethod() {
        console.log(this.name); // "Bob"

        const arrowFunction = () => {
            console.log(this.name); // "Bob" (inherited from regularMethod)
        };

        function regularFunction() {
            console.log(this.name); // undefined (new context)
        }

        arrowFunction();
        regularFunction();
    }
};

obj.regularMethod();

🔹 Manual Binding: bind, call, apply

You can manually set this using these methods:

call() - Immediate execution

Immediately invokes the function with a specific this value and passes arguments one by one.

javascript

function greet(greeting, punctuation) {
    console.log(`${greeting}, I'm ${this.name}${punctuation}`);
}

const person = { name: "Charlie" };
greet.call(person, "Hello", "!"); // "Hello, I'm Charlie!"

apply() - Immediate execution with array

Works exactly like call(), but takes arguments as an array instead of individually.

javascript

greet.apply(person, ["Hi", "."]); // "Hi, I'm Charlie."

bind() - Returns new function

Creates a new function with a permanently bound this value. It doesn't execute the function immediately.

javascript

const boundGreet = greet.bind(person);
boundGreet("Hey", "?"); // "Hey, I'm Charlie?"

3. Object-Oriented JavaScript

JavaScript's approach to OOP is unique because it's based on prototypes rather than traditional classes.

🔹 Constructor Functions & Prototypes (Pre-ES6)

Before ES6 classes, we used constructor functions:

javascript

function Person(name, age) {
    this.name = name;
    this.age = age;
}

// Adding methods to the prototype
Person.prototype.greet = function() {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old`;
};

Person.prototype.birthday = function() {
    this.age++;
    console.log(`Happy birthday! Now ${this.age} years old.`);
};

const alice = new Person("Alice", 25);
console.log(alice.greet()); // "Hi, I'm Alice and I'm 25 years old"
alice.birthday(); // "Happy birthday! Now 26 years old."

🔹The new Keyword Behavior

When you use new with a function, four things happen:

  1. A new empty object is created

  2. The new object's prototype is set to the constructor's prototype

  3. this is bound to the new object

  4. The new object is returned (unless the constructor explicitly returns something else)

javascript

function Car(brand) {
    // 1. New object created: {}
    // 2. Prototype set: {} → Car.prototype
    // 3. this bound to new object
    this.brand = brand;
    this.start = function() {
        console.log(`${this.brand} started`);
    };
    // 4. New object returned automatically
}

const toyota = new Car("Toyota");

🔹 ES6 Classes: Modern OOP

ES6 introduced class syntax that's more familiar to developers from other languages:

javascript

class Animal {
    constructor(name, species) {
        this.name = name;
        this.species = species;
    }

    makeSound() {
        console.log(`${this.name} makes a sound`);
    }

    describe() {
        return `${this.name} is a ${this.species}`;
    }
}

class Dog extends Animal {
    constructor(name, breed) {
        super(name, "Dog"); // Call parent constructor
        this.breed = breed;
    }

    makeSound() {
        console.log(`${this.name} barks!`);
    }

    fetch() {
        console.log(`${this.name} fetches the ball`);
    }
}

const buddy = new Dog("Buddy", "Golden Retriever");
console.log(buddy.describe()); // "Buddy is a Dog"
buddy.makeSound(); // "Buddy barks!"
buddy.fetch(); // "Buddy fetches the ball"

🔹 Prototypal vs Classical Inheritance

Prototypal Inheritance (JavaScript's native approach): Objects inherit directly from other objects through the prototype chain.

javascript

const animal = {
    makeSound() {
        console.log("Some generic animal sound");
    }
};

const dog = Object.create(animal);
dog.bark = function() {
    console.log("Woof!");
};

dog.makeSound(); // "Some generic animal sound" (inherited)
dog.bark(); // "Woof!" (own method)

Classical Inheritance (traditional OOP): Classes inherit from other classes. ES6 classes are syntactic sugar over prototypal inheritance.

🔹 Encapsulation with Private Fields

ES2022 introduced private class fields using the # symbol:

javascript

class BankAccount {
  #balance = 0; // private field
  deposit(amount) {
    this.#balance += amount;
  }
  getBalance() {
    return this.#balance;
  }
}

const acc = new BankAccount();
acc.deposit(500);
console.log(acc.getBalance()); // 500
// acc.#balance ❌ Error

4. Callbacks, Promises & Async/Await

Asynchronous programming is essential in JavaScript. Let's explore how it evolved from callbacks to modern async/await.

🔹 Callbacks: The Foundation

A callback function is a function passed as an argument to another function and executed after the main function completes.

javascript

function processData(data, callback) {
    console.log("Processing data...");
    setTimeout(() => {
        const result = data.toUpperCase();
        callback(result);
    }, 1000);
}

processData("hello world", function(result) {
    console.log("Result:", result); // "Result: HELLO WORLD"
});

Callback Hell: Nested callbacks become messy:

When you have multiple dependent asynchronous operations, callbacks can create deeply nested, hard-to-read code:

javascript

// The dreaded "Pyramid of Doom"
getUser(1, function(user) {
    getOrders(user.id, function(orders) {
        getOrderDetails(orders[0], function(details) {
            getShippingInfo(details.id, function(shipping) {
                getTrackingInfo(shipping.id, function(tracking) {
                    console.log("Finally got tracking:", tracking);
                    // 😱 This is getting out of hand!
                });
            });
        });
    });
});

🔹 Promises: The Solution

A Promise is an object representing the eventual completion (or failure) of an asynchronous operation.

Promise States

  • Pending → Initial state

  • Fulfilled → Success (.then())

  • Rejected → Failure (.catch())

javascript

// Creating a Promise
const myPromise = new Promise((resolve, reject) => {
    const success = Math.random() > 0.5;

    setTimeout(() => {
        if (success) {
            resolve("Operation successful!"); // ✅ Fulfilled
        } else {
            reject("Operation failed!"); // ❌ Rejected
        }
    }, 1000);
});

// Using the Promise
myPromise
    .then(result => {
        console.log("Success:", result);
        return "Next step";
    })
    .then(nextResult => {
        console.log("Chained:", nextResult);
    })
    .catch(error => {
        console.log("Error:", error);
    });

🔹 Async/Await

async/await is syntactic sugar built on top of Promises, allowing you to write asynchronous code that looks synchronous.

Basic Syntax

javascript

// async function always returns a Promise
async function fetchUserData() {
    return "User data"; // Automatically wrapped in Promise.resolve()
}

fetchUserData().then(data => console.log(data)); // "User data"

Using await

javascript

async function fetchDataWithAsync() {
    try {
        const response = await fetch('/api/data');

        if (!response.ok) {
            throw new Error('Network response was not ok');
        }

        const data = await response.json();
        return data;
    } catch (error) {
        console.log('Fetch error:', error);
        throw error;
    }
}

🎯 Key Takeaways

Scope & Closures

  • Use let and const for block scope

  • Closures enable data privacy and function factories

  • Understanding execution context helps debug scope issues

The this Keyword

  • this depends on how a function is called, not where it's defined

  • Arrow functions inherit this from the enclosing scope

  • Use bind, call, or apply for explicit this binding

Object-Oriented JavaScript

  • ES6 classes are syntactic sugar over prototypal inheritance

  • Use private fields (#) for true encapsulation

  • super keyword enables proper inheritance

Asynchronous Programming

  • Callbacks can lead to callback hell

  • Promises provide better error handling and chaining

  • async/await makes asynchronous code readable and maintainable

Understanding these advanced JavaScript concepts will significantly improve your ability to write clean, maintainable, and efficient code. These patterns form the foundation of modern JavaScript frameworks and libraries.

Happy coding! 🎉


This is Part 3 of our JavaScript Mastery series. Make sure to check out the previous parts and stay tuned for more advanced topics!