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";
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
var is function-scoped.
if (true) {
var x = 10;
}
console.log(x); // 10
Enter fullscreen mode Exit fullscreen mode
The variable is accessible outside the if block.
let
let is block-scoped.
if (true) {
let x = 10;
}
console.log(x); // ReferenceError
Enter fullscreen mode Exit fullscreen mode
A variable declared with let can be reassigned.
let age = 20;
age = 21;
console.log(age); // 21
Enter fullscreen mode Exit fullscreen mode
But it cannot be redeclared in the same scope.
let age = 20;
let age = 21; // SyntaxError
Enter fullscreen mode Exit fullscreen mode
const
const is also block-scoped.
const age = 20;
Enter fullscreen mode Exit fullscreen mode
It cannot be reassigned.
const age = 20;
age = 21; // TypeError
Enter fullscreen mode Exit fullscreen mode
However, const does not make an object completely immutable.
const user = {
name: "Koushik"
};
user.name = "Rahul";
console.log(user.name); // Rahul
Enter fullscreen mode Exit fullscreen mode
The object can still be modified. The variable itself cannot be reassigned to another object.
Quick Comparison
Featurevar
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;
Enter fullscreen mode Exit fullscreen mode
The output is:
undefined
Enter fullscreen mode Exit fullscreen mode
This can be understood roughly as:
var x;
console.log(x);
x = 10;
Enter fullscreen mode Exit fullscreen mode
The declaration is available before the assignment happens.
let and const
Now consider:
console.log(x);
let x = 10;
Enter fullscreen mode Exit fullscreen mode
This gives:
ReferenceError
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
Trying to access x before its declaration causes a ReferenceError.
Function Hoisting
Function declarations are also hoisted.
sayHello();
function sayHello() {
console.log("Hello");
}
Enter fullscreen mode Exit fullscreen mode
Output:
Hello
Enter fullscreen mode Exit fullscreen mode
However, function expressions assigned to let or const cannot be called before their declaration.
sayHello();
const sayHello = function () {
console.log("Hello");
};
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The inner() function can access variables from its own scope and from the outer scopes.
The scope chain looks like:
inner()
↓
outer()
↓
global scope
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
When two() is called, it is pushed onto the call stack.
Then two() calls one(), so one() is pushed on top.
| one() |
| two() |
| global |
------------
Enter fullscreen mode Exit fullscreen mode
one() finishes first, so it is removed.
| two() |
| global |
------------
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
Output:
Koushik
Enter fullscreen mode Exit fullscreen mode
Here:
this === user
Enter fullscreen mode Exit fullscreen mode
because the function was called as:
user.greet();
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Output:
Koushik
Enter fullscreen mode Exit fullscreen mode
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"]);
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
Output:
Koushik
Enter fullscreen mode Exit fullscreen mode
The simple difference is:
call() → calls the function immediately
apply() → calls the function immediately
bind() → returns a new function
Enter fullscreen mode Exit fullscreen mode
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);
Enter fullscreen mode Exit fullscreen mode
Output:
Koushik
Enter fullscreen mode Exit fullscreen mode
Conceptually, this happens:
new User("Koushik")
|
↓
Create a new object
|
↓
this points to that object
|
↓
this.name = "Koushik"
|
↓
Object is returned
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
The arrow function gets this from greet().
greet()
|
| this → user
|
└── arrow function
|
└── inherits this → user
Enter fullscreen mode Exit fullscreen mode
Therefore, the output is:
Koushik
Enter fullscreen mode Exit fullscreen mode
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();
Enter fullscreen mode Exit fullscreen mode
When the program starts, JavaScript creates the global execution context.
Then:
const fn = outer();
Enter fullscreen mode Exit fullscreen mode
calls outer().
A new function execution context is created.
Inside outer():
let name = "Outer";
Enter fullscreen mode Exit fullscreen mode
Then inner() is created.
outer() returns inner.
const fn = outer();
Enter fullscreen mode Exit fullscreen mode
Now fn refers to inner.
When we call:
fn();
Enter fullscreen mode Exit fullscreen mode
inner() can still access:
name = "Outer"
Enter fullscreen mode Exit fullscreen mode
even though outer() has already finished.
That’s because of the closure.
The output is:
Outer
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
For this, remember:
How was the function called?
|
┌─────┼─────────┬────────┐
↓ ↓ ↓ ↓
obj.fn call/apply new arrow
↓ ↓ ↓ ↓
implicit explicit new lexical
binding binding this this
Enter fullscreen mode Exit fullscreen mode
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!
답글 남기기