Modern JavaScript (ES6+)
Mind Map Summary
- Modern JavaScript (ES6/ES2015 and beyond)
- Variables:
let
: Block-scoped variables. The modern replacement forvar
.const
: Block-scoped constants. The value cannot be reassigned.
- Functions:
- Arrow Functions (
=>
): A concise syntax for functions. Lexically binds thethis
keyword, avoiding common bugs.
- Arrow Functions (
- Asynchronous Operations:
- Callbacks (Old Way): Prone to “Callback Hell” (deeply nested, hard-to-read code).
- Promises: An object representing the eventual success or failure of an async operation. Allows for cleaner
.then()
and.catch()
chaining. async
/await
(Modern Way): Syntactic sugar over Promises. Lets you write async code that looks and reads like synchronous code, which is much more intuitive.
- Modules:
- What: The ability to split code into separate files and share functionality between them.
- Keywords:
import
andexport
.
- The Event Loop:
- What: The core mechanism in JavaScript for handling asynchronous operations without blocking the main thread, enabling a responsive UI.
- Variables:
Core Concepts
1. let
and const
vs. var
Before ES6, var
was the only way to declare a variable. var
is function-scoped, which can lead to confusing behavior. let
and const
are block-scoped (they only exist within the nearest set of curly braces {}
), which is more predictable.
- Best Practice: Always use
const
by default. If you know you need to reassign the variable, uselet
. Avoid usingvar
in modern code.
2. Arrow Functions
The syntax is more concise ((a, b) => a + b
vs. function(a, b) { return a + b; }
), but the most important feature is how it handles this
. In a traditional function, the value of this
is determined by how the function is called. In an arrow function, this
is determined by the surrounding scope (lexical binding). This solves a whole class of bugs, especially in event handlers and callbacks.
3. Asynchronous JavaScript & The Event Loop
JavaScript is single-threaded, meaning it can only do one thing at a time. The Event Loop is the model that allows it to handle long-running operations (like an API call) without freezing the user interface.
- When an async operation (like
fetch
) is started, it’s handed off to the browser’s Web API. - The JavaScript call stack continues to run and stays clear.
- When the Web API finishes its work, it places a callback function in the Callback Queue.
- The Event Loop constantly checks: is the call stack empty? If it is, it takes the first item from the queue and pushes it onto the stack to be executed.
4. Promises and async/await
- Promises were introduced to clean up “Callback Hell”. A Promise is an object that holds the future result of an async operation. It can be in one of three states:
pending
,fulfilled
, orrejected
. You can chain.then()
to handle the success case and.catch()
to handle the error case. async/await
is a layer of syntactic sugar on top of Promises that makes async code even cleaner. Theasync
keyword on a function makes it automatically return a Promise. Theawait
keyword pauses the function execution until a Promise settles, and then resumes with the result. It lets you write asynchronous logic in a sequential, easy-to-read style.
Practice Exercise
Refactor a piece of callback-based asynchronous code (callback hell) to use Promises, and then again using async/await
. Explain the improvements in readability and error handling.
Answer
Let’s imagine a sequence of three asynchronous steps: fetching a user, then fetching their posts, then fetching the comments for the first post.
1. The “Bad” Code: Callback Hell
function getUser(id, callback) {
setTimeout(() => {
console.log("Fetched user");
callback(null, { id: id, name: "John Doe" });
}, 500);
}
function getPosts(userId, callback) {
setTimeout(() => {
console.log("Fetched posts");
callback(null, [{ id: 1, title: "Post 1" }, { id: 2, title: "Post 2" }]);
}, 500);
}
function getComments(postId, callback) {
setTimeout(() => {
console.log("Fetched comments");
callback(null, [{ id: 101, text: "Great post!" }]);
}, 500);
}
// Callback Hell - The Pyramid of Doom
getUser(1, (err, user) => {
if (err) {
console.error(err);
} else {
getPosts(user.id, (err, posts) => {
if (err) {
console.error(err);
} else {
getComments(posts[0].id, (err, comments) => {
if (err) {
console.error(err);
} else {
console.log("Comments:", comments);
}
});
}
});
}
});
- Readability: This is very hard to read due to the deep nesting (the “pyramid of doom”).
- Error Handling: Error handling is repetitive and verbose. You have to check for
err
at every single step.
2. The “Better” Code: Using Promises
First, we refactor our functions to return Promises.
function getUserP(id) {
return new Promise((resolve, reject) => {
setTimeout(() => { console.log("Fetched user"); resolve({ id: id, name: "John Doe" }); }, 500);
});
}
// ... similar Promise-based functions for getPostsP and getCommentsP ...
// Promise Chaining
getUserP(1)
.then(user => getPostsP(user.id))
.then(posts => getCommentsP(posts[0].id))
.then(comments => console.log("Comments:", comments))
.catch(err => console.error("An error occurred:", err));
- Readability: This is a huge improvement. The code is flattened into a clean, sequential chain of
.then()
calls. - Error Handling: Much better. A single
.catch()
at the end of the chain will handle any error that occurs in any of the preceding steps.
3. The “Best” Code: Using async/await
// We can reuse the same Promise-based functions from the previous example.
async function fetchPostComments(userId) {
try {
const user = await getUserP(userId);
const posts = await getPostsP(user.id);
const comments = await getCommentsP(posts[0].id);
console.log("Comments:", comments);
} catch (err) {
console.error("An error occurred:", err);
}
}
fetchPostComments(1);
- Readability: This is the most readable and intuitive version. It looks almost exactly like synchronous code, making it very easy to follow the logic. The
await
keyword makes it clear where the code is pausing to wait for an asynchronous operation to complete. - Error Handling: Excellent. We can use the standard
try...catch
block, which is a familiar and powerful construct for handling errors in synchronous code. It naturally handles errors from any of theawait
ed promises within thetry
block.