Week 3 - Task 1: JavaScript Core Concepts
JavaScript has many concepts that look simple at first but become very important when writing real applications. While learning JavaScript, I came across concepts like var, let, const, hoisting, lexical scope, execution context, call stack, closures, and this.
In this blog, I’m sharing my understanding of these concepts with simple examples.
1. Variables: var, let, and const
Variables are used to store data in JavaScript.
var name = "Koushik";
let age = 20;
const city = "Hyderabad";
Although all three can be used to declare variables, they behave differently.
var
var is the older way of declaring variables in JavaScript.
var age = 20;
age = 21;
console.log(age); // 21
var is function-scoped.
if (true) {
var x = 10;
}
console.log(x); // 10
The variable is accessible outside the if block.
let
let is block-scoped.
if (true) {
let x = 10;
}
console.log(x); // ReferenceError
A variable declared with let can be reassigned.
let age = 20;
age = 21;
console.log(age); // 21
But it cannot be redeclared in the same scope.
let age = 20;
let age = 21; // SyntaxError
const
const is also block-scoped.
const age = 20;
It cannot be reassigned.
const age = 20;
age = 21; // TypeError
However, const does not make an object completely immutable.
const user = {
name: "Koushik"
};
user.name = "Rahul";
console.log(user.name); // Rahul
The object can still be modified. The variable itself cannot be reassigned to another object.
Quick Comparison
| Feature | var |
let |
const |
|---|---|---|---|
| Scope | Function | Block | Block |
| Reassignment | Yes | Yes | No |
| Redeclaration | Yes | No | No |
| Hoisted | Yes | Yes | Yes |
| Temporal Dead Zone | No | Yes | Yes |
For modern JavaScript, let and const are generally preferred over var.
2. Hoisting
Hoisting is the behavior where JavaScript processes declarations before executing the code in that scope.
For example:
console.log(x);
var x = 10;
The output is:
undefined
This can be understood roughly as:
var x;
console.log(x);
x = 10;
The declaration is available before the assignment happens.
let and const
Now consider:
console.log(x);
let x = 10;
This gives:
ReferenceError
let and const are also hoisted, but they cannot be accessed before their declaration is evaluated.
This period is called the Temporal Dead Zone (TDZ).
// TDZ starts
let x = 10;
// TDZ ends
Trying to access x before its declaration causes a ReferenceError.
Function Hoisting
Function declarations are also hoisted.
sayHello();
function sayHello() {
console.log("Hello");
}
Output:
Hello
However, function expressions assigned to let or const cannot be called before their declaration.
sayHello();
const sayHello = function () {
console.log("Hello");
};
This results in a ReferenceError.
3. Lexical Scope
Lexical scope means that the scope of a variable is determined by where the code is written.
Consider:
let name = "Koushik";
function outer() {
let age = 20;
function inner() {
console.log(name);
console.log(age);
}
inner();
}
outer();
The inner() function can access variables from its own scope and from the outer scopes.
The scope chain looks like:
inner()
↓
outer()
↓
global scope
This is called the scope chain.
JavaScript uses lexical scoping, which is also one of the main reasons closures work.
4. Execution Context
An execution context is the environment in which JavaScript code is executed.
There are mainly two execution contexts that are important to understand:
- Global Execution Context
- Function Execution Context
When JavaScript starts running a program, it creates the Global Execution Context.
For example:
let name = "Koushik";
function greet() {
console.log("Hello");
}
greet();
First, JavaScript creates the global execution context.
When greet() is called, JavaScript creates another execution context for that function.
Global Execution Context
|
↓
greet()
|
↓
Function Execution Context
Creation and Execution
Execution can be simplified into two phases:
- Creation phase
- Execution phase
During the creation phase, JavaScript prepares the environment for variables, functions, scope information, and this.
During the execution phase, JavaScript executes the code.
5. Call Stack
The call stack keeps track of which functions are currently being executed.
Consider:
function one() {
console.log("One");
}
function two() {
one();
console.log("Two");
}
two();
When two() is called, it is pushed onto the call stack.
Then two() calls one(), so one() is pushed on top.
| one() |
| two() |
| global |
------------
one() finishes first, so it is removed.
| two() |
| global |
------------
Then two() finishes.
The call stack follows LIFO:
Last In, First Out
This is why the most recently called function finishes first.
Recommended Video
For a clearer understanding of Execution Context and the Call Stack, I recommend watching this YouTube tutorial:
Execution Context & Call Stack – JavaScript Tutorial
This video helped me understand how JavaScript executes code and how the call stack manages function calls.
6. Closures
Closures are one of the most interesting concepts in JavaScript.
A closure happens when a function remembers variables from its outer lexical scope even after the outer function has finished executing.
Example:
function outer() {
let count = 0;
function inner() {
count++;
console.log(count);
}
return inner;
}
const counter = outer();
counter(); // 1
counter(); // 2
counter(); // 3
At first, outer() finishes execution.
Normally, we might expect count to disappear.
But inner() still needs access to count.
Because inner() was created inside outer(), it remembers the environment where it was created.
outer()
|
| count = 0
|
└── inner()
|
└── remembers count
This is a closure.
Why Are Closures Useful?
Closures are commonly used for:
- Maintaining state
- Data privacy
- Counters
- Function factories
- Callbacks
- Event handlers
For example:
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 count variable is private to the closure.
7. Understanding this
The this keyword is another important JavaScript concept.
A common beginner mistake is thinking:
"
thisalways refers to the object where the function was created."
For normal functions, this is not necessarily true.
The value of this depends mainly on how the function is called.
Some important cases are:
- Implicit binding
- Explicit binding
-
newbinding - Arrow functions
8. Implicit Binding
When a function is called as a method of an object, this refers to that object.
const user = {
name: "Koushik",
greet() {
console.log(this.name);
}
};
user.greet();
Output:
Koushik
Here:
this === user
because the function was called as:
user.greet();
The object before the dot determines the this value in this case.
9. Explicit Binding
JavaScript provides three methods that can be used to control this:
call()apply()bind()
call()
function greet() {
console.log(this.name);
}
const user = {
name: "Koushik"
};
greet.call(user);
Output:
Koushik
call() immediately invokes the function with the specified this.
apply()
apply() works similarly to call(), but arguments are passed as an array.
function introduce(age, city) {
console.log(this.name, age, city);
}
const user = {
name: "Koushik"
};
introduce.apply(user, [20, "Hyderabad"]);
bind()
bind() creates a new function with this permanently bound to the provided object.
function greet() {
console.log(this.name);
}
const user = {
name: "Koushik"
};
const newGreet = greet.bind(user);
newGreet();
Output:
Koushik
The simple difference is:
call() → calls the function immediately
apply() → calls the function immediately
bind() → returns a new function
10. new Binding
The new keyword creates a new object and makes that object the this value inside the constructor function.
function User(name) {
this.name = name;
}
const user1 = new User("Koushik");
console.log(user1.name);
Output:
Koushik
Conceptually, this happens:
new User("Koushik")
|
↓
Create a new object
|
↓
this points to that object
|
↓
this.name = "Koushik"
|
↓
Object is returned
So user1 becomes an instance of User.
11. Arrow Functions and this
Arrow functions behave differently from normal functions.
An arrow function does not have its own this.
Instead, it inherits this from its surrounding lexical scope.
For example:
const user = {
name: "Koushik",
greet() {
setTimeout(() => {
console.log(this.name);
}, 1000);
}
};
user.greet();
The arrow function gets this from greet().
greet()
|
| this → user
|
└── arrow function
|
└── inherits this → user
Therefore, the output is:
Koushik
This is one reason arrow functions are very useful for callbacks.
However, arrow functions should not be used when you specifically need a function to have its own dynamic this.
12. Putting the Concepts Together
Let's look at an example that combines lexical scope, execution context, closures, and the call stack.
var name = "Global";
function outer() {
let name = "Outer";
function inner() {
console.log(name);
}
return inner;
}
const fn = outer();
fn();
When the program starts, JavaScript creates the global execution context.
Then:
const fn = outer();
calls outer().
A new function execution context is created.
Inside outer():
let name = "Outer";
Then inner() is created.
outer() returns inner.
const fn = outer();
Now fn refers to inner.
When we call:
fn();
inner() can still access:
name = "Outer"
even though outer() has already finished.
That's because of the closure.
The output is:
Outer
Final Mental Model
These concepts are connected.
JavaScript starts
|
↓
Global Execution Context
|
↓
Code starts executing
|
↓
Function is called
|
↓
New Execution Context
|
↓
Function pushed onto Call Stack
|
↓
Variables resolved using Lexical Scope
|
↓
Nested functions can create Closures
|
↓
Function finishes
|
↓
Execution Context removed from Call Stack
|
↓
Closure can keep required variables accessible
For this, remember:
How was the function called?
|
┌─────┼─────────┬────────┐
↓ ↓ ↓ ↓
obj.fn call/apply new arrow
↓ ↓ ↓ ↓
implicit explicit new lexical
binding binding this this
Key Takeaways
-
varis function-scoped, whileletandconstare block-scoped. - Hoisting happens when JavaScript sets up a scope before executing it.
-
letandconsthave a Temporal Dead Zone. - Lexical scope is determined by where the code is written.
- Execution contexts provide the environment for code execution.
- The call stack manages function execution using LIFO.
- Closures allow functions to remember variables from their outer scope.
-
thisdepends on how a normal function is called. -
call(),apply(), andbind()provide explicit control overthis. -
newcreates a new object and bindsthisto it. - Arrow functions don't have their own
this; they inherit it from the surrounding scope.
These concepts helped me understand that JavaScript is not just about writing statements one after another. There is a whole execution process happening behind the scenes, and understanding that process makes it much easier to reason about JavaScript code and debug unexpected behavior.
Also if anyone wants my handwritten notes on this core topic you can comment on this blog. Thank You!
Top comments (1)
been waiting for someone to build this. MCP is great but the token cost was killing me on big tool sets.