Hi, I’m Abhilash! A seasoned web developer with 15 years of experience specializing in Ruby and Ruby on Rails. Since 2010, I’ve built scalable, robust web applications and worked with frameworks like Angular, Sinatra, Laravel, Node.js, Vue and React.
Passionate about clean, maintainable code and continuous learning, I share insights, tutorials, and experiences here. Let’s explore the ever-evolving world of web development together!
Ruby provides a powerful way to handle exceptions using the begin block. One of the key features of this block is ensure, which ensures that a certain section of code runs no matter what happens in the begin block. This is particularly useful when dealing with resource management, such as file handling, database connections, and network requests.
Understanding begin, rescue, and ensure
The begin block in Ruby is used to handle potential exceptions. It works alongside rescue, which catches exceptions, and ensure, which executes code regardless of whether an exception occurs.
Basic Syntax:
begin
# Code that might raise an exception
rescue SomeError => e
# Handle the exception
ensure
# Code that will always execute
end
Resource Cleanup – Ensures that resources like file handles, database connections, or network sockets are properly closed.
Prevents Leaks – Helps avoid memory or resource leaks by making sure cleanup is performed.
Example 1: File Handling
One of the most common uses of ensure is closing a file after performing operations.
file = nil
begin
file = File.open("example.txt", "r")
puts file.read
rescue StandardError => e
puts "An error occurred: #{e.message}"
ensure
file.close if file
puts "File closed."
end
Explanation:
The begin block opens a file and reads its contents.
If an error occurs (e.g., file not found), the rescue block catches it.
The ensure block ensures that the file is closed, preventing resource leaks.
Example 2: Database Connection Handling
Handling database connections properly is crucial to avoid locked or hanging connections.
require 'sqlite3'
db = nil
begin
db = SQLite3::Database.open("test.db") # Open database connection
db.execute("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)")
db.execute("INSERT INTO users (name) VALUES ('Alice')")
puts "User added successfully."
rescue SQLite3::Exception => e
puts "Database error: #{e.message}"
ensure
db.close if db # Ensure the database connection is closed
puts "Database connection closed."
end
Explanation:
Opens a database connection and executes SQL statements.
If an error occurs, such as a syntax error in SQL, rescue catches it.
The ensure block ensures the database connection is closed, preventing connection leaks.
Example 3: Network Request Handling
When making HTTP requests, errors like timeouts or invalid URLs can occur. Using ensure, we can ensure proper handling.
require 'net/http'
url = URI("http://example.com")
response = nil
begin
response = Net::HTTP.get(url)
puts "Response received: #{response[0..50]}..." # Print a snippet of the response
rescue StandardError => e
puts "Network error: #{e.message}"
ensure
puts "Request complete. Cleanup actions (if any) can be performed here."
end
Explanation:
Makes an HTTP request to a given URL.
If an error occurs (e.g., network failure), rescue handles it.
The ensure block ensures any necessary final actions, such as logging, happen.
Key Takeaways
The ensure block always executes, making it essential for cleanup tasks.
It helps prevent resource leaks by ensuring proper closure of files, database connections, and network requests.
Using ensure makes your Ruby code robust and reliable, handling errors gracefully while ensuring necessary actions take place.
By incorporating ensure in your Ruby code, you can improve reliability, maintainability, and efficiency in handling critical resources.
Ruby is a dynamically typed language that favors duck typing over strict type enforcement. However, there are cases where type checking can be useful to avoid unexpected behavior. In this post, we’ll explore various ways to perform type validation and type checking in Ruby.
Type Checking and Type Casting in Ruby
Yes, even though Ruby does not enforce types at the language level, there are several techniques to validate the types of method parameters. Below are some approaches:
1. Manual Type Checking with raise
One straightforward way to enforce type checks is by manually verifying the type of a parameter using is_a? and raising an error if it does not match the expected type.
def my_method(arg)
raise TypeError, "Expected String, got #{arg.class}" unless arg.is_a?(String)
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(123) # Raises: TypeError: Expected String, got Integer
2. Using respond_to? for Duck Typing
Rather than enforcing a strict class type, we can check whether an object responds to a specific method.
def my_method(arg)
unless arg.respond_to?(:to_str)
raise TypeError, "Expected a string-like object, got #{arg.class}"
end
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(:symbol) # Raises TypeError
3. Using Ruby 3’s Type Signatures (RBS)
Ruby 3 introduced RBS and TypeProf for static type checking. You can define types in an .rbs file:
def my_method: (String) -> void
Then, you can use tools like steep, a static type checker for Ruby, to enforce type checking at development time.
How to Use Steep for Type Checking
Steep does not use annotations or perform type inference on its own. Instead, it relies on .rbi files to define type signatures. Here’s how you can use Steep for type checking:
Define a Ruby Class:
class Calculator
def initialize(value)
@value = value
end
def double
@value * 2
end
end
Generate an .rbi File:
steep scaffold calculator.rb > sig/calculator.rbi
This generates an .rbi file, but initially, it will use any for all types. You need to manually edit it to specify proper types.
Modify the .rbi File to Define Types:
class Calculator
@value: Integer
def initialize: (Integer) -> void
def double: () -> Integer
end
Run Steep to Check Types:
steep check
Steep also supports generics and union types, making it a powerful but less intrusive type-checking tool compared to Sorbet.
4. Using Sorbet for Stronger Type Checking
Sorbet is a third-party static type checker that allows you to enforce type constraints at runtime.
require 'sorbet-runtime'
extend T::Sig
sig { params(arg: String).void }
def my_method(arg)
puts "Valid input: #{arg}"
end
my_method("Hello") # Works fine
my_method(123) # Raises error at runtime
Another Approach: Using Rescue for Type Validation
A different way to handle type checking is by using exception handling (rescue) to catch unexpected types and enforce validation.
def process_order(order_items, customer_name, discount_code)
# Main logic
...
rescue => e
# Type and validation checks
raise "Expecting an array of items: #{order_items.inspect}" unless order_items.is_a?(Array)
raise "Order must contain at least one item: #{order_items.inspect}" if order_items.empty?
raise "Expecting a string for customer name: #{customer_name.inspect}" unless customer_name.is_a?(String)
raise "Customer name cannot be empty" if customer_name.strip.empty?
raise "Unexpected error in `process_order`: #{e.message}"
end
Summary
Use is_a? or respond_to? for runtime type checking.
Use Ruby 3’s RBS for static type enforcement.
Use Sorbet for stricter type checking at runtime.
Use Steep for static type checking with RBS.
Exception handling can be used for validating types dynamically.
Additional Considerations
Ruby is a dynamically typed language, and unit tests can often be more effective than type checks in ensuring correctness. Writing tests ensures that method contracts are upheld for expected data.
For Ruby versions prior to 3.0, install the rbs gem separately to define types for classes.
If a method is defined, it will likely be called. If reasonable tests exist, every method will be executed and checked. Therefore, instead of adding excessive type checks, investing time in writing tests can be a better strategy.
Ruby on Rails is a powerful framework for building web applications. If you’re setting up your development environment on macOS in 2025, this guide will walk you through installing Ruby 3.4, Rails 8, and a best IDE for development.
1. Installing Ruby and Rails
“While macOS comes with Ruby pre-installed, it’s often outdated and can’t be upgraded easily. Using a version manager like Mise allows you to install the latest Ruby version, switch between versions, and upgrade as needed.” – Rails guides
Install Dependencies
Run the following command to install essential dependencies (takes time):
zsh completions have been installed to: /opt/homebrew/share/zsh/site-functions ==> Summary 🍺 /opt/homebrew/Cellar/rust/1.84.1: 3,566 files, 321.3MB ==> Running brew cleanup rust… ==> openssl@3 A CA file has been bootstrapped using certificates from the system keychain. To add additional certificates, place .pem files in /opt/homebrew/etc/openssl@3/certs
and run /opt/homebrew/opt/openssl@3/bin/c_rehash ==> rust zsh completions have been installed to: /opt/homebrew/share/zsh/site-functions
By following this guide, you’ve successfully set up a robust Ruby on Rails development environment on macOS. With Mise for version management, Rails installed, and VS Code configured with essential extensions, you’re ready to start building Ruby on Rails applications.
If you’re setting up your MacBook for development, having a well-configured terminal is essential. This guide will walk you through installing and configuring a powerful terminal setup using Homebrew, iTerm2, and Oh My Zsh, along with useful plugins.
1. Install Homebrew
Homebrew is a package manager that simplifies installing software on macOS.
Your terminal is now set up for an optimized development experience! With Homebrew, iTerm2, Oh My Zsh, and useful plugins, your workflow will be faster and more efficient.
When a client request comes into a Rails application, it doesn’t always go directly to the MVC (Model-View-Controller) layer. Instead, it might first pass through middleware, which handles tasks such as authentication, logging, and static asset management.
Rails uses middleware like ActionDispatch::Static to efficiently serve static assets before they even reach the main application.
ActiveRecord: Object-relational mapping (ORM) system for database interactions.
Action Pack: Handles the controller and view layers.
Active Support: A collection of utility classes and standard library extensions.
Action Mailer: A framework for designing email services.
The Role of Browsers in Asset Management
Web browsers cache static assets to improve performance. The caching strategy varies based on asset types:
Images: Rarely change, so they are aggressively cached.
JavaScript and CSS files: Frequently updated, requiring cache-busting mechanisms.
The Era of Sprockets
Historically, Rails used Sprockets as its default asset pipeline. Sprockets provided:
Conversion of CoffeeScript to JavaScript and SCSS to CSS.
Minification and bundling of assets into fewer files.
Digest-based caching to ensure updated assets were fetched when changed.
The Rise of JavaScript & The Shift Towards Webpack
The release of ES6 (2015-2016) was a turning point for JavaScript, fueling the rise of Single Page Applications (SPAs). This marked a shift from traditional asset management:
Sprockets was effective but became complex and difficult to configure for modern JS frameworks.
Projects started including package.json at the root, indicating JavaScript dependency management.
Webpack emerged as the go-to tool for handling JavaScript, offering features like tree-shaking, hot module replacement, and modern JavaScript syntax support.
The Landscape in 2024: A More Simplified Approach
Recent advancements in web technology have drastically simplified asset management:
ES6 Native Support in All Major Browsers
No need for transpilation of modern JavaScript.
CSS Advancements
Features like variables and nesting eliminate the need for preprocessors like SASS.
HTTP/2 and Multiplexing
Enables parallel loading of multiple assets over a single connection, reducing dependency on bundling strategies.
Enter Propshaft: The Modern Asset Pipeline
Propshaft is the new asset management solution introduced in Rails, replacing Sprockets for simpler and faster asset handling. Key benefits include:
Digest-based file stamping for effective cache busting.
Direct and predictable mapping of assets without complex processing.
Better integration with HTTP/2 for efficient asset delivery.
Rails 8 Precompile Uses Propshaft
What is Precompile? A Reminder
Precompilation hashes all file names and places them in the public/ folder, making them accessible to the public.
Propshaft improves upon this by creating a manifest file that maps the original filename as a key and the hashed filename as a value. This significantly enhances the developer experience in Rails.
Propshaft ultimately moves asset management in Rails to the next level, making it more efficient and streamlined.
The Future of Asset Management in Rails
With advancements like native ES6 support and CSS improvements, Rails continues evolving to embrace simpler, more efficient asset management strategies. Propshaft, combined with modern browser capabilities, makes asset handling seamless and more performance-oriented.
As the web progresses, we can expect further simplifications in asset pipelines, making Rails applications faster and easier to maintain.
Stay tuned for more innovations in the Rails ecosystem!
This is one of the most important JavaScript lessons for us as a Ruby/Rails developer.
After this lesson, we should be able to read code like:
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
and understand exactly what this means.
We’ll also understand why:
const greet = user.greet;
can behave differently from:
user.greet();
Then we’ll connect that to:
useEffect(()=>{
...
});
and eventually to closures, which are fundamental to React and Node.js.
1. What is this?
The easiest starting point is:
this is a special value available inside a function.
But unlike Ruby’s self, its value is usually determined by how a regular function is called.
Consider:
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
user.greet();
Output:
John
Why?
Because we called:
user.greet();
The object before the . is the receiver.
So:
user.greet()
↓
this = user
Therefore:
this.name
is effectively:
user.name
2. Think about the call site
This is the most useful mental model:
user.greet();
Look at the left side of the dot:
user.greet()
^^^^
receiver
For a normal method call, that receiver becomes this.
Example:
constperson={
name:"Alice",
sayName(){
console.log(this.name);
}
};
person.sayName();
Here:
this → person
Output:
Alice
3. But this is not permanently attached to the function
This is where JavaScript differs from the simple mental model we may have from Ruby.
Take:
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
Now:
user.greet();
gives:
John
But:
const greet = user.greet;
greet();
is a different call.
We did:
const greet = user.greet;
So greet now refers to the function.
But we’re no longer calling it as:
user.greet()
We’re calling:
greet()
There is no receiver.
So the this behavior is different.
4. The three things we should distinguish
When we see a function, distinguish:
user.greet
user.greet()
const fn = user.greet;
fn();
They mean different things.
user.greet
Get the function.
user.greet()
Call the function with user as the method receiver.
fn()
Call the function independently.
This distinction is extremely important in JavaScript.
5. this in a regular function
Consider:
functionshowThis(){
console.log(this);
}
What this is depends on how we invoke the function.
That’s the core idea:
regular function
↓
how was it called?
↓
determines `this`
There are several invocation patterns, and we’ll learn the important ones.
6. Method call
The easiest:
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
user.greet();
Here:
this = user
7. Explicitly set this
JavaScript gives us:
call()
apply()
bind()
These let us control what this refers to.
This is a very common int. topic.
8. call
Suppose:
functiongreet(){
console.log(this.name);
}
We have an object:
constuser={
name:"John"
};
We can explicitly say:
greet.call(user);
Now:
this = user
Therefore:
John
So:
greet.call(user);
means approximately:
Call greet now, and make this equal to user.
9. call with arguments
Suppose:
functiongreet(message){
console.log(`${message}, ${this.name}`);
}
Then:
const user = {
name: "John"
};
Call:
greet.call(user, "Hello");
Output:
Hello, John
The structure is:
function.call(thisArg, arg1, arg2, ...)
For example:
greet.call(user, "Hello");
means:
this = user
message = "Hello"
10. apply
apply is similar to call.
greet.apply(user, ["Hello"]);
Output:
Hello, John
The difference is mainly how arguments are supplied.
call
greet.call(user, "Hello", "How are you?");
apply
greet.apply(user, ["Hello", "How are you?"]);
So:
call → arguments individually
apply → arguments as an array
Historically apply was particularly useful when we already had an array of arguments. With modern JavaScript, spread syntax has reduced some of those use cases.
11. bind
bind is different.
const boundGreet = greet.bind(user);
This does not immediately call greet.
Instead it creates a new function.
Think:
greet.bind(user)
↓
new function
↓
this permanently set to user
Then:
boundGreet("Hello");
Output:
Hello, John
So:
call → call now
apply → call now
bind → create another function
That’s the key distinction.
12. call vs apply vs bind
Memorize this table:
Method
Executes immediately?
Arguments
call
Yes
Individual
apply
Yes
Array
bind
No
Returns new function
Example:
fn.call(obj, 1, 2);
fn.apply(obj, [1, 2]);
const newFn = fn.bind(obj);
newFn(1, 2);
13. A practical example
Suppose:
constuser1={
name:"John"
};
constuser2={
name:"Jane"
};
functiongreet(message){
console.log(`${message}, ${this.name}`);
}
Now:
greet.call(user1, "Hello");
Output:
Hello, John
And:
greet.call(user2, "Hello");
Output:
Hello, Jane
Same function.
Different this.
This demonstrates why this in regular functions is associated with the invocation.
14. Arrow functions change the rules
Now comes a critical concept.
Arrow functions do not get their own this.
They use this from the surrounding lexical scope.
Example:
constuser={
name:"John",
greet:()=>{
console.log(this.name);
}
};
We might think:
user.greet()
↓
this = user
But that’s not how an arrow function works.
An arrow function does not create its own this.
It captures this from the surrounding scope.
15. Why this matters
Compare:
Regular function
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
user.greet();
Here:
this = user
Arrow function
constuser={
name:"John",
greet:()=>{
console.log(this.name);
}
};
Here this does not become user.
This is why object methods generally should not be written as arrow functions when they need dynamic this.
16. The classic callback problem
Now we get to a very common JavaScript issue.
Suppose:
constuser={
name:"John",
greet(){
setTimeout(function(){
console.log(this.name);
},1000);
}
};
user.greet();
We might expect:
John
But the inner regular function has its own this behavior.
The setTimeout callback isn’t being called as:
user.someMethod()
So it doesn’t automatically inherit the outer method’s this.
17. Arrow functions solve this nicely
Now:
constuser={
name:"John",
greet(){
setTimeout(()=>{
console.log(this.name);
},1000);
}
};
user.greet();
Now the arrow function:
() => {
console.log(this.name);
}
doesn’t create a new this.
It captures the this from greet().
So:
user.greet()
↓
this = user
↓
arrow callback
↓
uses surrounding this
↓
this = user
Output:
John
This is one of the biggest reasons arrow functions are so useful.
18. Arrow functions and React
We’ll often see:
<button onClick={() => handleClick()}>
Click
</button>
Here the arrow function is primarily being used as a callback.
React will call the arrow function later when the event occurs.
Similarly:
useEffect(() => {
loadUsers();
}, []);
The callback passed to useEffect is an arrow function.
Again, remember:
() => {
...
}
is just a function.
React receives that function and decides when to invoke it.
19. Now: closures
This is perhaps even more important than this.
A closure happens when a function remembers variables from its surrounding lexical scope.
Consider:
function createCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
Call:
const counter = createCounter();
Now:
counter(); // 1
counter(); // 2
counter(); // 3
The amazing part is:
createCounter() has already finished executing.
Yet the returned function can still access:
count
Why?
Because the function closed over the variable.
20. Visualize the closure
When we execute:
const counter = createCounter();
think:
createCounter()
|
| creates
v
count = 0
|
| returns function
v
counter ────────→ function
|
└── remembers count
Then:
counter();
the function can still access the remembered count.
After:
counter();
the value becomes:
count = 1
Next:
counter();
the function still has access to the same variable:
count = 2
21. Closure does NOT mean copying the value
This is an important detail.
Some beginners imagine:
count = 0
gets copied into the function.
That’s not the best mental model.
Instead, the function retains access to the lexical environment containing count.
Therefore:
counter();
counter();
counter();
all operate on the same captured variable.
22. Why closures are useful
Closures allow us to keep private state.
Example:
functioncreateBankAccount(initialBalance){
letbalance=initialBalance;
return{
deposit(amount){
balance+=amount;
},
getBalance(){
returnbalance;
}
};
}
Now:
const account = createBankAccount(1000);
account.deposit(500);
console.log(account.getBalance());
Result:
1500
But:
account.balance
doesn’t exist as a public property.
The state is captured by the closure.
This is conceptually similar to encapsulation.
23. Closures and callbacks
Callbacks frequently create closures.
Example:
Output:
Hello John
The returned callback remembers:
name = "John"
even after greetUser() finished.
So:
outer function
↓
creates variable
↓
creates callback
↓
callback remembers variable
That’s a closure.
24. Closures in loops
Here’s a classic int. example:
for (var i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
Many people expect:
0
1
2
but with var, all callbacks refer to the same function-scoped i, which has reached 3 when the callbacks run.
So we get:
3
3
3
This is a classic closure + asynchronous execution int. question.
Now change var to let:
for (let i = 0; i < 3; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
Now the callbacks see the per-iteration i values:
0
1
2
This connects directly to our Lesson 2 discussion of let and block/iteration scoping.
25. Closure + React
Closures are everywhere in React.
For example:
function User({ user }) {
const handleClick = () => {
console.log(user.name);
};
return (
<button onClick={handleClick}>
Click
</button>
);
}
The function:
handleClick
uses:
user
from its surrounding scope.
So it forms a closure over user.
This is normal JavaScript behavior, not React-specific magic.
26. Closure + useEffect
Consider:
function UserComponent({ userId }) {
useEffect(() => {
console.log(userId);
}, [userId]);
return null;
}
The callback passed to useEffect accesses:
userId
from the surrounding function scope.
That’s a closure.
This is why closures and React hooks are closely related.
Later, when we study the dependency array deeply, this will become very important.
27. Closure + Node.js
Closures are equally useful in Node.
For example:
function createLogger(prefix) {
return message => {
console.log(`[${prefix}] ${message}`);
};
}
const errorLogger = createLogger("ERROR");
const infoLogger = createLogger("INFO");
errorLogger("Database failed");
infoLogger("Server started");
Output:
[ERROR] Database failed
[INFO] Server started
Each returned function remembers its own prefix.
So closures are not merely “React stuff.”
They’re a fundamental JavaScript feature.
28. this vs closure
These are different concepts.
this
Answers:
What object/context does this regular function call use as this?
Closure
Answers:
What variables from surrounding lexical scopes can this function still access?
For example:
const user = {
name: "John",
greet() {
const message = "Hello";
setTimeout(() => {
console.log(this.name);
console.log(message);
}, 1000);
}
};
The arrow callback has access to:
this
message
but for different reasons:
this → inherited from surrounding context
message → closure over surrounding lexical variable
This distinction is worth remembering.
29. Ruby comparison
Ruby has:
self
JavaScript has:
this
But don’t assume:
Ruby self == JavaScript this
They have important differences.
Ruby self is much more naturally tied to the current execution context/object.
JavaScript this for a regular function is heavily dependent on how the function is called.
And JavaScript arrow functions don’t create their own this.
That distinction is especially important when moving from Rails backend code to React/Node.
30. A very useful mental model
For regular functions, ask:
How was this function called?
For arrow functions, ask:
What is the surrounding `this`?
For closures, ask:
Which variables from outer scopes does this function use?
These three questions will solve a huge number of JavaScript puzzles.
🎯 Interview questions
Try these without running them.
Q1
What is this here?
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
user.greet();
Q2
What’s the problem here?
const greet = user.greet;
greet();
Q3
What’s the difference between:
greet.call(user);
and:
const fn = greet.bind(user);
Q4
What is the difference between call and apply?
Q5
What is a closure?
Q6
Why does this work?
function createCounter() {
let count = 0;
return () => ++count;
}
const counter = createCounter();
counter(); // ?
counter(); // ?
Q7
Why does var produce the classic loop problem?
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
🧪 Live coding practice
Problem 1 – this
Predict the output:
constuser={
name:"John",
greet(){
return`Hello ${this.name}`;
}
};
console.log(user.greet());
Problem 2 – call
Write:
function introduce(role) {
console.log(`${this.name} is a ${role}`);
}
Then use call so that:
const user = {
name: "Abhilash"
};
produces:
Abhilash is a Senior Software Engineer
Problem 3 – bind
Given:
constuser={
name:"John"
};
functiongreet(message){
console.log(`${message}, ${this.name}`);
}
Create:
const boundGreet = ...
so that:
boundGreet("Hello");
produces:
Hello, John
Problem 4 – Closure
Implement:
const counter = createCounter();
counter(); // 1
counter(); // 2
counter(); // 3
The count variable should not be directly accessible from outside.
Problem 5 – Closure with argument
Create:
const double = multiplier(2);
double(5); // 10
double(10); // 20
The function multiplier(2) should return another function.
This is an excellent exercise for understanding closures.
Problem 6 – Callback + closure
Implement:
function createGreeter(name) {
return function() {
console.log(`Hello ${name}`);
};
}
Then:
const greetJohn = createGreeter("John");
const greetJane = createGreeter("Jane");
greetJohn();
greetJane();
Expected:
Hello John
Hello Jane
Be able to explain why each returned function remembers a different name.
Final mental model
We now have four layers:
FUNCTION
↓
Function can be passed around as a value
CALLBACK
↓
A function passed to another function
THIS
↓
For regular functions, determined by how the function is called
CLOSURE
↓
A function retains access to variables from its lexical scope
And the most important distinctions:
user.greet
↓
function reference
user.greet()
↓
function call
↓
this = user
greet.call(user)
↓
call now with explicit this
greet.apply(user, args)
↓
call now with explicit this + array arguments
greet.bind(user)
↓
return a new function with bound this
For arrow functions:
arrow function
↓
doesn't create its own `this`
↓
captures surrounding `this`
For closures:
outer scope
↓
function created inside it
↓
inner function remembers outer variables
The connection to React
We can now look at:
functionUser({user}){
consthandleClick=()=>{
console.log(user.name);
};
return(
<button onClick={handleClick}>
Click
</button>
);
}
and identify:
{ user } → object destructuring
handleClick → function
() => → arrow function
onClick={handleClick}→ function reference
user.name → closure over user
That’s the exact JavaScript foundation we want before going further into React.
Next lesson: JavaScript asynchronous programming – synchronous execution, call stack, event loop, Web/Node APIs, callback queue, microtask queue, Promises, and why async/await works. This will be especially important for us because it connects JavaScript directly to Node.js.
This lesson connects several things you’ve already seen:
const[users,setUsers]=useState([]);
and:
const{name,age}=user;
and React props such as:
<User name="John"age={30}/>
The goal is to understand how JavaScript objects work and how we extract, copy, and pass their data around.
1. JavaScript objects
An object is a collection of properties:
constuser={
id:1,
name:"John",
age:30,
active:true
};
Think of it as:
user
|
+--id → 1
+--name → "John"
+--age → 30
+--active → true
Access properties:
user.name;// "John"
user.age;// 30
user.active;// true
You can also use bracket notation:
user["name"];
These are equivalent when the property name is known.
2. Dot notation vs bracket notation
This works:
constuser={
name:"John"
};
console.log(user.name);
But bracket notation becomes particularly useful when the property name is stored in a variable:
constproperty="name";
console.log(user[property]);
Result:
John
This would not work the same way:
user.property;
because that looks for a property literally called "property".
3. Objects can contain functions
For example:
constuser={
name:"John",
greet(){
console.log("Hello");
}
};
Call it:
user.greet();
The function is a property of the object.
When a function belongs to an object like this, we commonly call it a method.
4. Objects can contain anything
An object can contain:
constuser={
id:1,
name:"John",
active:true,
roles:["admin","editor"],
address:{
city:"Kochi",
country:"India"
},
greet(){
console.log("Hello");
}
};
So you can have:
object
├── primitive
├── array
├── object
└── function
This is why JavaScript objects are extremely flexible.
5. Object destructuring
Now the first very important ES6 feature.
Suppose:
constuser={
id:1,
name:"John",
age:30
};
You could write:
constname=user.name;
constage=user.age;
But destructuring allows:
const{name,age}=user;
This means:
constname=user.name;
constage=user.age;
Conceptually, yes.
So:
const{name,age}=user;
means:
Take the`name`property from user
↓
put it into variable`name`
Take the`age`property from user
↓
put it into variable`age`
6. Array destructuring vs object destructuring
This is one of the most important distinctions from the previous lessons.
Array
constnumbers=[10,20];
const[a,b]=numbers;
Destructuring is based on position:
numbers[0] → a
numbers[1] → b
Object
constuser={
name:"John",
age:30
};
const{name,age}=user;
Destructuring is based on property name:
user.name → name
user.age → age
So:
Array destructuring → position
Object destructuring → property name
This distinction should become automatic.
7. Rename while destructuring
Suppose:
constuser={
name:"John",
age:30
};
You don’t have to name your variable name.
You can do:
const{name:userName,age:userAge}=user;
Now:
userName; // "John"
userAge; // 30
Read this as:
name property → variable userName
age property → variable userAge
This syntax looks strange initially, but you’ll see it in real React and Node code.
8. Default values in destructuring
Suppose:
constuser={
name:"John"
};
You can write:
const { name, age = 30 } = user;
Now:
name;// "John"
age;// 30
The default is used only when the property is undefined.
9. Nested destructuring
Given:
constuser={
name:"John",
address:{
city:"Kochi",
country:"India"
}
};
You can do:
const{
name,
address:{city,country}
}=user;
Now:
name; // "John"
city; // "Kochi"
country; // "India"
This is useful, but don’t overuse deep destructuring. Very complex destructuring can become harder to read than ordinary property access.
10. Function parameters can be destructured
This is very common in React.
Instead of:
functionUser(props){
return <h2>{props.name}</h2>;
}
you can write:
functionUser({name}){
return <h2>{name}</h2>;
}
The second form says:
The function receives an object, and I want its name property.
Suppose React effectively gives:
{
name:"John",
age:30
}
Then:
functionUser({name}){
extracts:
name="John";
This is one of the reasons you see React code full of { ... } around function parameters.
11. useState vs props: two different destructuring styles
Now compare these:
const[users,setUsers]=useState([]);
and:
functionUser({name,age}){
}
The first is array destructuring:
position 0 → users
position 1 → setUsers
The second is object destructuring:
name property → name
age property → age
This is a very important React distinction.
12. Spread syntax with objects
Let’s revisit spread:
constuser={
name:"John",
age:30
};
constcopy={
...user
};
Now:
copy
contains:
{
name:"John",
age:30
}
The spread operator copies the object’s own enumerable properties into a new object.
At a beginner level, think:
“Take the properties from this object and put them into a new object.”
13. Updating an object using spread
Suppose:
constuser={
name:"John",
age:30
};
We want:
same user
but age = 31
Write:
constupdatedUser={
...user,
age:31
};
Result:
{
name:"John",
age:31
}
Why does age: 31 win?
Object properties appearing later overwrite earlier properties with the same key.
So conceptually:
...user
↓
name: John
age: 30
age: 31
↓
overwrite age
14. This is common in React state updates
Suppose:
const[user,setUser]=useState({
name:"John",
age:30
});
To change just the age:
setUser({
...user,
age:31
});
We don’t do:
user.age=31;
for normal React state updates.
Instead, we create a new object.
Conceptually:
old object
↓
{name:"John",age:30}
...copy...
newobject
↓
{name:"John",age:31}
This gives React a new object reference.
We’ll later connect this directly to the question you raised previously about why React cares about new references.
15. Spread with arrays
Given:
constusers=["John","Jane"];
Append without modifying the original:
constnewUsers=[...users,"Mike"];
Result:
["John","Jane","Mike"]
Prepend:
constnewUsers=["Mike",...users];
Result:
["Mike","John","Jane"]
Merge:
constfirst=[1,2];
constsecond=[3,4];
constcombined=[...first,...second];
Result:
[1,2,3,4]
16. Spread is shallow
This is an important interview topic.
Consider:
constuser={
name:"John",
address:{
city:"Kochi"
}
};
constcopy={
...user
};
copy is a new object.
But:
copy.address===user.address
is:
true
Why?
Because spread creates a shallow copy.
The outer object is new, but nested objects are still shared.
Visualize:
user ────────────────┐
│
copy ────────────────┤
↓
address
{city:"Kochi"}
Both objects point to the same nested address object.
This becomes extremely important when we talk about immutable state.
17. Deep copy is a different problem
Don’t assume:
constcopy={...user};
means:
“Everything inside user has been recursively copied.”
It hasn’t.
For modern applications there are several strategies for deep cloning, but don’t jump to one automatically. Often you don’t actually need a deep clone.
For React state, it’s usually better to create new references only along the part of the structure you’re changing.
For example:
constupdatedUser={
...user,
address:{
...user.address,
city:"Bengaluru"
}
};
Now both the outer object and changed nested object are new references.
18. Object shorthand
ES6 gives us convenient syntax here:
constname="John";
constage=30;
constuser={
name,
age
};
Instead of:
constuser={
name:name,
age:age
};
JavaScript understands that:
name
means:
name: name
when used in an object literal.
You’ll see this everywhere.
19. Computed property names
You can dynamically create a property name:
constfield="name";
constuser={
[field]:"John"
};
Result:
{
name:"John"
}
Another example:
constfield="email";
constvalue="john@example.com";
constuser={
[field]:value
};
Result:
{
email:"john@example.com"
}
This is useful when dynamically building objects.
20. this – the important part
Now we reach one of JavaScript’s most confusing concepts for Ruby developers:
this
Consider:
constuser={
name:"John",
greet(){
console.log(this.name);
}
};
user.greet();
Output:
John
Here:
this
refers to the object used to call the method:
user.greet();
So conceptually:
user.greet()
↓
this=user
Therefore:
this.name
is:
user.name
21. A critical difference from Ruby
In Ruby, you may mentally think:
“self is the current object.”
JavaScript’s this is more nuanced.
One of the most useful rules:
For a normal function call, this is determined by how the function is called, not simply where the function was defined.
That means:
user.greet();
and:
constgreet=user.greet;
greet();
can have different this behavior.
This is one of the reasons JavaScript’s this can be tricky.
22. Arrow functions and this
Arrow functions behave differently.
They do not create their own this.
Consider:
constuser={
name:"John",
greet:()=>{
console.log(this.name);
}
};
user.greet();
Do not assume:
this=user
just because the function appears inside the object.
That is not how arrow functions work.
This is one of the reasons you should not blindly replace every regular function with an arrow function.
We’ll spend an entire lesson on this, call, apply, bind, and arrow-function behavior.
23. Object references
Now an important JavaScript behavior.
constuser1={
name:"John"
};
constuser2=user1;
We did not create a new object.
Both variables refer to the same object:
user1 ─────┐
↓
{name:"John"}
↑
user2 ─────┘
Therefore:
user2.name="Jane";
console.log(user1.name);
prints:
Jane
This is because objects are reference values.
24. Compare primitive values
Now compare:
leta=10;
letb=a;
b=20;
console.log(a);
Result:
10
For the primitive number:
a → 10
b → 10
then:
b → 20
Changing b doesn’t alter a.
With objects:
user1 ──┐
↓
object
↑
user2 ──┘
Both references point to the same object.
This distinction will become crucial when we discuss:
React state
equality
immutability
shallow comparison
useMemo
useCallback
25. === with objects
This gives us another important interview question.
constuser1={name:"John"};
constuser2={name:"John"};
console.log(user1===user2);
Result:
false
Why?
Because these are two different objects.
Even though their contents are identical:
user1 → Object A
user2 → Object B
They have different references.
But:
constuser1={name:"John"};
constuser2=user1;
user1===user2;
returns:
true
because both reference the same object.
26. This explains something important in React
Later you’ll see code like:
setUsers(users);
versus:
setUsers([...users]);
The first gives React the same array reference.
The second creates a new array:
users ───────→ old array
[...users] ──→ new array
This distinction becomes important because React can use reference identity when determining whether state/props values changed.
We’ll go deep into this when we revisit React state and rendering.
27. Complete React example
Let’s put today’s concepts together.
function User({ user }) {
const { name, age } = user;
return (
<div>
<h2>{name}</h2>
<p>{age}</p>
</div>
);
}
Here we have:
Object parameter
{ user }
Object destructuring
const { name, age } = user;
Function
function User(...) {
}
And React passes an object as the argument.
28. Another React example
Suppose:
const [user, setUser] = useState({
name: "John",
age: 30,
city: "Kochi"
});
Update only the city:
setUser({
...user,
city: "Bengaluru"
});
This combines:
object
+
spread
+
new object reference
+
state update
And now you can see why learning JavaScript first is useful.
🧠 The five concepts to remember
At the end of this lesson, these should be clear:
1. Object destructuring
const { name, age } = user;
Means:
const name = user.name;
const age = user.age;
2. Array destructuring
const [first, second] = numbers;
Means:
const first = numbers[0];
const second = numbers[1];
3. Spread
const copy = { ...user };
Creates a new shallow object.
4. References
const b = a;
with objects means both variables refer to the same object.
5. this
For a normal method call:
user.greet();
this inside the method refers to the receiver of that call – user.
Arrow functions have different this behavior.
🎯 Int. questions
Try answering these before reading further.
Q1
What’s the difference?
const [a, b] = [10, 20];
and:
const { a, b } = { a: 10, b: 20 };
Q2
What does this produce?
const user = {
name: "John",
age: 30
};
const { name: userName } = user;
Q3
What is the difference between:
const user2 = user1;
and:
const user2 = { ...user1 };
Q4
What happens here?
const user1 = { name: "John" };
const user2 = { name: "John" };
console.log(user1 === user2);
Q5
What does this do?
const user = {
name: "John",
age: 30
};
const updated = {
...user,
age: 31
};
Q6
Why can this be dangerous to assume?
const copy = { ...user };
when user contains nested objects?
🧪 Live coding practice
Problem 1 – Destructure
Given:
const user = {
id: 10,
name: "John",
age: 30
};
Extract:
id
name
age
using object destructuring.
Problem 2 – Rename
Extract name into a variable called:
userName
Problem 3 – Update immutably
Given:
const user = {
name: "John",
age: 30
};
Create a new object where:
name = "Jane"
without modifying user.
Problem 4 – Add a property
Given:
const user = {
name: "John"
};
Create:
{
name: "John",
active: true
}
using spread.
Problem 5 – Nested object
Given:
const user = {
name: "John",
address: {
city: "Kochi",
country: "India"
}
};
Create a new object where only:
city = "Bengaluru"
changes.
Don’t mutate the original nested address.
Problem 6 – Reference test
Predict:
const a = { count: 1 };
const b = a;
b.count = 2;
console.log(a.count);
Then compare:
const a = { count: 1 };
const b = { ...a };
b.count = 2;
console.log(a.count);
Explain why the outputs differ.
🔗 How this connects to your React learning
You now have almost all the JavaScript pieces needed to understand this:
const [users, setUsers] = useState([]);
and:
function User({ name, age }) {
and:
setUser({
...user,
age: 31
});
They are no longer “React syntax”. They are combinations of ordinary JavaScript:
This is the first lesson where JavaScript will behave noticeably differently from Ruby, and it is especially important for understanding React event handlers and Node.js code.
This syntax becomes much easier once destructuring and rest are clear.
28. A complete example
Let’s combine everything.
constusers=[
{id:1,name:"John",active:true,age:30},
{id:2,name:"Jane",active:false,age:25},
{id:3,name:"Mike",active:true,age:35}
];
Find active users
constactiveUsers=users.filter(user=>user.active);
Get their names
constnames=activeUsers.map(user=>user.name);
Get total age
consttotalAge=activeUsers.reduce(
(sum,user)=>sum+user.age,
0
);
Result:
activeUsers
// John, Mike
names
// ["John", "Mike"]
totalAge
// 65
The whole pipeline:
users
│
├── filter ──→ active users
│
├── map ─────→ names
│
└── reduce ──→ total age
💎 Ruby comparison
These should look familiar:
Ruby:
users.select { |user| user[:active] }
JavaScript:
users.filter(user=>user.active);
Ruby:
users.map { |user| user[:name] }
JavaScript:
users.map(user=>user.name);
Ruby:
users.find { |user| user[:id] ==2 }
JavaScript:
users.find(user=>user.id===2);
Ruby:
users.reduce(0) { |sum, user| sum+user[:age] }
JavaScript:
users.reduce((sum,user)=>sum+user.age,0);
The collection-processing concepts are familiar. The biggest thing to master is JavaScript’s function syntax and callback model.
⚠️ One important distinction: map does NOT modify the original array
constnumbers=[1,2,3];
constdoubled=numbers.map(n=>n*2);
console.log(numbers);
console.log(doubled);
Output:
[1, 2, 3]
[2, 4, 6]
The original remains unchanged.
Similarly:
constfiltered=numbers.filter(n=>n>1);
creates another array.
This is one reason these methods fit well with React’s preference for immutable state updates.
🎯 Int. questions
Try answering these yourself.
Q1
What’s the difference between:
forEach()
map()
filter()
find()
reduce()
Q2
What does this return?
[1,2,3,4].map(n=>n*2);
Q3
What does this return?
[1,2,3,4].filter(n=>n%2===0);
Q4
What does this return?
[10,20,30].find(n=>n>15);
Q5
What is the final value?
[10,20,30].reduce(
(sum,n)=>sum+n,
0
);
Q6
What is the difference between spread and rest?
constcopy=[...numbers];
versus:
functiontest(...numbers){}
🧪 Live coding practice
Try solving these without using a loop first.
Problem 1 – Double numbers
doubleNumbers([1,2,3,4]);
// [2, 4, 6, 8]
Use map.
Problem 2 – Active users
getActiveUsers(users);
Return only users where:
user.active===true
Use filter.
Problem 3 – Find user
findUser(users,2);
Return the user whose id is 2.
Use find.
Problem 4 – Total
sum([10,20,30,40]);
// 100
Use reduce.
Problem 5 – Method chaining
Given:
constusers=[
{name:"John",active:true},
{name:"Jane",active:false},
{name:"Mike",active:true},
{name:"Sarah",active:false}
];
Produce:
["John","Mike"]
using:
filter + map
Problem 6 – Spread
Given:
constusers=["John","Jane"];
Create:
["John","Jane","Mike"]
without using push.
Problem 7 – Rest
Write:
sum(10,20,30,40);
and make it return:
100
using a rest parameter.
Mental model for today
Remember these seven statements:
forEach → do something for every item
map → transform every item
filter → keep matching items
find → get the first matching item
some → does at least one match?
every → do all match?
reduce → accumulate/build one result
And:
spread → expand values
rest → collect values
The most important connection from today’s lesson is:
Array method
↓
takes a callback
↓
callback runs for each relevant item
↓
method produces a result
For example:
users
.filter(user=>user.active)
.map(user=>user.name);
You should now be able to read that almost as English:
“Take users, keep the active ones, then transform each remaining user into their name.”
Next lesson: Objects, destructuring, spread, this, and JavaScript’s object model. This will connect directly to React props and explain why const { name } = user and const [users, setUsers] = useState([]) look similar but work differently.
Here the callback is asynchronous because setTimeout schedules it to run later.
This distinction is critical:
callback
≠
async
A callback may be:
synchronous
or
asynchronous
13. Higher-order functions
A function that:
accepts a function as an argument, or
returns a function
is commonly called a higher-order function.
Example:
functionexecute(callback){
callback();
}
execute is a higher-order function because it accepts a function.
Another example:
functioncreateGreeter(){
returnfunction(){
console.log("Hello");
};
}
createGreeter is also a higher-order function because it returns a function.
14. This explains map
Now something you’ll see constantly in React:
constnumbers=[1,2,3];
constdoubled=numbers.map(n=>n*2);
What is actually happening?
map receives a function:
n=>n*2
Conceptually:
numbers.map(callback)
↓
function(n){
returnn*2;
}
For every element, map calls your callback.
Conceptually:
1 → callback(1) → 2
2 → callback(2) → 4
3 → callback(3) → 6
Result:
[2,4,6]
15. map with objects
This is extremely important for React.
constusers=[
{id:1,name:"John"},
{id:2,name:"Jane"}
];
constnames=users.map(user=>user.name);
The callback:
user=>user.name
runs for every user.
Result:
["John","Jane"]
You can think:
users
↓
map(callback)
↓
John → "John"
Jane → "Jane"
↓
["John", "Jane"]
16. filter
filter also accepts a callback.
constnumbers=[1,2,3,4,5];
constresult=numbers.filter(n=>n>2);
The callback returns a boolean:
1 → false
2 → false
3 → true
4 → true
5 → true
Result:
[3,4,5]
The pattern is:
array.filter(callback)
17. find
constusers=[
{id:1,name:"John"},
{id:2,name:"Jane"}
];
constuser=users.find(user=>user.id===2);
The callback is:
user=>user.id===2
Result:
{id:2,name:"Jane"}
18. Why React uses callbacks everywhere
Consider:
<buttononClick={()=>console.log("Clicked")}>
Click me
</button>
You’re giving React a function:
()=>console.log("Clicked")
You’re essentially saying:
“When the click happens, call this function.”
That is a callback.
Another example:
users.map(user=>(
<divkey={user.id}>
{user.name}
</div>
))
Again:
user=>(...)
is a callback.
So when you see React code filled with arrow functions, don’t think:
“React magic.”
Think:
“JavaScript is passing functions around.”
That mental model is much more useful.
19. A very important React mistake
Compare:
<buttononClick={handleClick}>
and:
<buttononClick={handleClick()}>
These are not the same.
First
onClick={handleClick}
means:
Give React the function. React will call it when the event occurs.
Second
onClick={handleClick()}
means:
Call the function right now and give the result to React.
This is one of the most common beginner mistakes.
Again:
function reference
handleClick
↓
"Here is the function"
function call
handleClick()
↓
"Execute it now"
20. Function parameters are just variables
Look at:
functiongreet(name){
console.log(name);
}
When you call:
greet("John");
JavaScript effectively does:
name = "John"
Similarly:
functionexecute(callback){
callback();
}
When:
execute(myFunction);
conceptually:
callback = myFunction
Then:
callback();
calls it.
This is the key to understanding callbacks.
21. Callback with data
Callbacks can receive values too.
functiongetUser(callback){
constuser={
id:1,
name:"John"
};
callback(user);
}
Usage:
getUser(user=>{
console.log(user.name);
});
Execution:
getUser()
↓
create user
↓
callback(user)
↓
user => console.log(user.name)
Output:
John
This pattern is foundational to asynchronous JavaScript.
💎 Ruby developer comparison
You already know something conceptually similar in Ruby:
[1, 2, 3].map { |n| n*2 }
JavaScript:
[1,2,3].map(n=>n*2);
Ruby:
users.map { |user| user[:name] }
JavaScript:
users.map(user=>user.name);
The syntax is different, but the underlying idea is similar:
collection
↓
iterate
↓
execute supplied function for each item
↓
produce result
But don’t equate Ruby blocks and JavaScript callbacks completely. JavaScript functions are first-class values in a particularly explicit way, and the language’s treatment of closures, this, arguments and async execution differs.
Int. questions
Try these before checking the answers.
Q1
What’s the difference?
handleClick
vs
handleClick()
Q2
What does this return?
constadd=(a,b)=>a+b;
Q3
Why does this return undefined?
constadd=(a,b)=>{
a+b;
};
Q4
What is a callback?
Q5
Are callbacks always asynchronous?
Q6
Why is map considered a higher-order-function use case?
Q7
What does this produce?
constnumbers=[1,2,3];
constresult=numbers.map(n=>n*10);
Mini exercise
Predict the output:
functioncalculate(a,b,callback){
constresult=a+b;
callback(result);
}
calculate(10,20,result=>{
console.log(result);
});
Then trace this one:
functionexecute(callback){
console.log("A");
callback();
console.log("B");
}
execute(()=>{
console.log("C");
});
Expected order:
?
?
?
And finally:
constusers=[
{id:1,name:"John",active:true},
{id:2,name:"Jane",active:false},
{id:3,name:"Mike",active:true}
];
constactiveUsers=users.filter(user=>user.active);
constnames=activeUsers.map(user=>user.name);
console.log(names);
Try to mentally execute it as:
users
↓
filter(callback)
↓
active users
↓
map(callback)
↓
names
What you should take away
At this point, you should have these mental models:
function → a value
function() → call the function
callback → function passed to another function
higher-order function → accepts/returns functions
arrow function → concise function syntax
map/filter/find → receive callbacks
The next lesson should build directly on this:
Lesson 4 – Arrays, map, filter, find, reduce, spread and rest, with lots of React-style examples. That lesson will make expressions like users.map(...) feel natural rather than mysterious.
This lesson is important because JavaScript variable scope behaves differently enough from Ruby to cause bugs and int. confusion.
The four things to understand:
let
const
scope
hoisting
let, const, Scope and Hoisting
1. let vs const
Start with the simplest rule:
constname="Abhilash";
letage=40;
Use const when the variable should not be reassigned:
constname="Abhilash";
// name = "John"; ❌
Use let when reassignment is required:
letage=40;
age=41;// ✅
Important: const does NOT make the value immutable
This is a very important JavaScript int. question.
constusers=[];
users.push("John");// ✅
The array can be modified.
What cannot happen is:
users=["Jane"];// ❌
Why?
Because const prevents reassignment of the variable, not mutation of the object.
Think:
constusers
|
v
[ "John" ]
You cannot make users point somewhere else:
users─────X────> [ "Jane" ]
But you can modify the existing array:
users
|
v
["John","Jane"]
The same applies to objects:
constuser={
name:"John"
};
user.name="Jane";// ✅
But:
user={};// ❌
Int. answer
const prevents reassignment, but it does not make objects or arrays immutable.
2. What is scope?
Scope simply means:
Where can this variable be accessed?
Example:
constname="Abhilash";
functiongreet(){
console.log(name);
}
greet();
name is available inside greet() because functions can access variables from their outer scope.
But:
functiongreet(){
constmessage="Hello";
}
console.log(message);// ❌
message exists only inside the function.
Visualize:
Global Scope
│
├── name
│
└── greet()
│
└── message
greet() can see name.
The outside cannot see message.
3. Block scope
This is where let and const become important.
A block is anything inside { }, such as:
if(...)
{
}
or:
for(...)
{
}
Example:
if(true){
constmessage="Hello";
console.log(message);// ✅
}
console.log(message);// ❌
message is block scoped.
Same with let:
if(true){
letage=40;
}
console.log(age);// ❌
4. Why var is different
Historically JavaScript used:
varname="John";
var does not have block scope.
Example:
if(true){
varname="John";
}
console.log(name);// John
That surprises many developers.
Compare:
if(true){
letname="John";
}
console.log(name);// ❌
This is one major reason modern JavaScript prefers let and const.
5. Ruby comparison
You may initially think:
iftrue
name="John"
end
putsname
Ruby’s local-variable behavior is different from JavaScript’s block scoping rules.
For JavaScript, remember:
const + let
↓
block scoped
and:
var
↓
function scoped
You don’t need to use var in modern code, but you must understand it when reading legacy JavaScript.
6. Nested scope
Scopes can be nested.
constcountry="India";
functionouter(){
conststate="Kerala";
functioninner(){
constcity="Kochi";
console.log(country);
console.log(state);
console.log(city);
}
inner();
}
inner() can access:
country
state
city
because it can look outward through its scope chain.
But outer() cannot access city.
Visualize:
Global
│
├── country
│
└── outer
│
├── state
│
└── inner
│
└── city
JavaScript searches for variables from the current scope outward.
This is called the scope chain.
7. A very important example
Look carefully:
constname="Abhilash";
functiongreet(){
constmessage=`Hello ${name}`;
console.log(message);
}
greet();
Inside greet():
name
is not declared locally.
JavaScript looks outward:
greet scope
↓
global scope
↓
find name
This behavior becomes extremely important when we learn closures.
8. What is hoisting?
Now we get to a classic int. topic.
JavaScript processes declarations before executing the code in the current scope.
This behavior is commonly called hoisting.
But don’t interpret it as JavaScript literally moving your code to the top. That’s a useful mental model, but the actual mechanics are more nuanced.
Start with a function declaration:
greet();
functiongreet(){
console.log("Hello");
}
This works.
Why?
Function declarations are available before their textual position.
9. var and hoisting
Consider:
console.log(name);
varname="John";
You might expect an error.
Instead:
undefined
A useful mental model is:
varname;
console.log(name);
name="John";
The declaration is available, but the assignment happens later.
10. let and const are different
Now:
console.log(name);
letname="John";
This produces:
ReferenceError
And:
console.log(name);
constname="John";
also produces:
ReferenceError
This is often explained using the Temporal Dead Zone (TDZ).
11. Temporal Dead Zone
For let and const, the variable exists in the scope before its declaration is executed, but you cannot access it before that point.
Example:
console.log(age);// ❌
letage=40;
The area between entering the scope and reaching the declaration is called the:
Temporal Dead Zone
You don’t need to memorize the implementation details yet. The int.-level mental model is:
var
↓
hoisted + initialized as undefined
let / const
↓
hoisted but inaccessible until declaration is reached
12. Function declarations vs function expressions
This becomes important when we get to callbacks.
This works:
greet();
functiongreet(){
console.log("Hello");
}
But this does not:
greet();
constgreet=function(){
console.log("Hello");
};
And similarly:
greet();
constgreet=()=>{
console.log("Hello");
};
The second forms involve a const variable, so the TDZ applies.
13. A common int. trap
What does this print?
varx=10;
if(true){
varx=20;
}
console.log(x);
Answer:
20
Because var is function scoped, not block scoped.
Now:
letx=10;
if(true){
letx=20;
}
console.log(x);
Answer:
10
The inner x belongs to the block.
Visualize:
let x = 10
│
├── if block
│ └── let x = 20
│
└── outside -> x is still 10
14. Shadowing
JavaScript allows an inner scope to define a variable with the same name.
constname="Abhilash";
functiontest(){
constname="John";
console.log(name);
}
test();
console.log(name);
Output:
John
Abhilash
The inner variable shadows the outer one.
This is perfectly valid, although unnecessary shadowing can make code harder to read.
15. Why this matters in React
Consider:
functionUserList(){
const[users,setUsers]=useState([]);
if(users.length===0){
constmessage="No users";
return<p>{message}</p>;
}
return(
<ul>
...
</ul>
);
}
message exists only inside that if block.
React code frequently contains nested blocks, callbacks and functions, so understanding scope prevents many bugs.
16. Scope + callbacks = important later
Consider:
functioncreateCounter(){
letcount=0;
returnfunction(){
count++;
console.log(count);
};
}
Don’t worry about understanding every detail yet.
The interesting question is:
How can the returned function still access count after createCounter() has finished?
That question leads directly to:
closures.
And closures are one of the most important JavaScript concepts for React and Node.js.
Qn) Is it necessary to put semi colon in Javascript?
No, putting a semicolon (or colon) at the end of a line is not mandatory in ES6 (ECMAScript 2015) or modern JavaScript.
JavaScript uses a feature called Automatic Semicolon Insertion (ASI). This means the JavaScript engine automatically inserts semicolons where it thinks they are needed to run your code correctly.
⚠️ The Rare Exception
While you can safely omit semicolons 99% of the time, there are rare cases where leaving them out can break your code. This usually happens if a line starts with a bracket [ or parenthesis (.
Example of a bug without semicolons:
constuser="Alice"
['a','b'].forEach(letter=>console.log(letter))
The JavaScript engine reads this as a single continuous line: const user = "Alice"['a', 'b'].forEach(...), which throws an error.
Best Practice
Because of ASI, choosing to use semicolons is largely a matter of personal or team preference:
With semicolons: Safer for beginners, prevents accidental ASI bugs, and follows traditional coding styles.
Without semicolons: Keeps code looking clean, modern, and reduces visual clutter.
Most development teams use a tool like Prettier or ESLint to automatically format the code and handle semicolons for them.
The most important thing from this lesson is to build the habit of asking:
Which scope does this variable belong to?
That question will help enormously when we reach closures, callbacks, useEffect, event handlers and async JavaScript.
Next lesson: Functions in depth – function declarations, expressions, arrow functions, parameters, return values, callbacks and higher-order functions.