JavaScript is a single-threaded, non-blocking, asynchronous language. To understand how it handles concurrent operations, it is important to understand several key components:
The JavaScript runtime consists of two main components:
🚨 Loupe - JavaScript's call stack/event loop/callback queue visualizer
The call stack is single-threaded, meaning it can only execute one operation at a time. It's a data structure that records the current position in the program. When you:
💡 The stack trace shows the state of the stack when an error is encountered.
Blocking occurs when operations that take significant time (like network requests or image processing) prevent the execution of subsequent code. The call stack is blocked until such requests are complete before moving on to the next piece of code.
JavaScript handles asynchronous operations through several mechanisms:
Callbacks are functions that another function calls, either synchronously or asynchronously.
// Synchronous callback example
[1, 2, 3, 4].forEach((i) => {
console.log(i);
});
// Asynchronous callback example - setTimeout is non-blocking
function asyncForEach(array, cb) {
array.forEach((i) => {
setTimeout(cb, 0);
});
}
The browser is more than just the JS runtime - it consists of Web APIs that are essentially threads that can be called. When a Web API is called:
The Task Queue is where callbacks wait to be executed. The Event Loop's job is to:
Micro-tasks have higher priority than the regular task queue:
The main thread is where:
Functional programming (declarative programming) expresses everything within the program as a function. The main idea is to avoid side effects and use pure functions.
Functions that:
// Pure function example
function greet(name) {
return "Hi, I'm " + name;
}
Functions that either:
function makeAdjectifier(adjective) {
return function (string) {
return adjective + " " + string;
};
}
const coolifier = makeAdjectifier("cool");
console.log(coolifier("conference")); // "cool conference"
for
or while
loops, prefer higher-order functions like map
, filter
, reduce
// Immutable array transformation
const rooms = ["room1", "room2", "room3", "room4"];
const newRooms = rooms.map((room) => (room === "room4" ? "room5" : room));
// [ 'room1', 'room2', 'room3', 'room5' ]
A new copy of the data is always made every time. One of the solutions to this is to use persistent data structures. Persistent data structures can be implemented using a technique called "structural sharing"
. This technique involves creating new versions of data structures that share as much of their underlying structure with the previous version as possible, rather than creating a completely new copy. Immutable js
and mori js
implement persistent data structure with this technique. Another technique is called path copying
which copies only a part of the data structure that is being modified this can be done with libraries like immer js
Solutions for performance issues with immutability:
Javascript supports OOP through its implementation of objects
and constructors
. The basic concepts of OOP in JS include objects, classes, inheritance and encapsulation.
OBJECT - is a collection of properties and methods that can be used to represent real-world objects. Properties are the data or state of the object and methods are the actions or behaviours that the object can perform.
CONSTRUCTORS - This allows an object template to be created. The constructor sets the initial methods and properties for an object or class and the new
keyword is used to create new instances of that object.
INHERITANCE - Inheritance is JS is implemented though a prototype chain
. Every object has a prototype which is an object from which it inherits its methods.
ENCAPSULATION - in JavaScript refers to the practice of hiding the internal details of an object, and only exposing a public interface for interacting with it. In JavaScript, encapsulation is typically achieved through closures
or by defining getters
and setters
for object properties.
// Using class syntax
class Person {
#name;
#age;
constructor(name, age) {
this.#name = name;
this.#age = age;
}
speak() {
console.log(`Hello, my name is ${this.#name}`);
}
// Getter
get name() {
return this.#name;
}
// Setter
set name(newName) {
this.#name = newName;
}
}
// Using object literals
const person = {
name: "John Doe",
age: 30,
speak() {
console.log(`Hello, my name is ${this.name}`);
},
};
class Student extends Person {
#major;
constructor(name, age, major) {
super(name, age);
this.#major = major;
}
study() {
console.log(`${this.name} is studying ${this.#major}`);
}
}
Code that executes sequentially on the JavaScript runtime, blocking until each operation completes.
Code that executes non-blocking operations, allowing other code to run while waiting for operations to complete.
console.log("Start");
function loginUser(email, password, callback) {
setTimeout(() => {
callback({ email, password });
}, 5000);
}
function getUserVideos(email, callback) {
setTimeout(() => {
callback(["video1", "video2", "video3", "video4"]);
}, 2000);
}
loginUser("user@example.com", "123456", (user) => {
console.log(user);
getUserVideos(user.email, (videos) => {
console.log(videos);
});
});
console.log("Finish");
function loginUser(email, password) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ email, password });
}, 5000);
});
}
function getUserVideos(email) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(["video1", "video2", "video3", "video4"]);
}, 2000);
});
}
// Promise chaining
loginUser("user@example.com", "123456")
.then((user) => getUserVideos(user.email))
.then((videos) => console.log(videos))
.catch((error) => console.error(error));
// Async/await syntax
async function getContent() {
try {
const user = await loginUser("user@example.com", "123456");
const videos = await getUserVideos(user.email);
return videos;
} catch (error) {
console.error(error);
}
}
function getUserVideos() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ videos: ["video1", "video2", "video3", "video4"] });
}, 3000);
});
}
function getUserTracks() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ songs: ["song1", "song2", "song3", "song4"] });
}, 6000);
});
}
// Run promises concurrently
Promise.all([getUserVideos(), getUserTracks()])
.then(([videos, tracks]) => {
console.log(videos, tracks);
})
.catch((error) => console.error(error));
A closure is a function that maintains access to its creation scope, even after the parent function has returned. They are created at function creation time.
function makeCounter() {
let count = 0;
return {
increment() {
return ++count;
},
decrement() {
return --count;
},
getCount() {
return count;
},
};
}
const counter = makeCounter();
console.log(counter.increment()); // 1
console.log(counter.increment()); // 2
console.log(counter.getCount()); // 2
In Node.js, a module is a self-contained unit of code that exports one or more properties and methods, making them available to other parts of the application. Each file in a Node.js application is considered a module with its own scope, which means that the variables and functions defined in that module are not available outside of it unless they are explicitly exported.
Modules in Node.js use the CommonJS module system, which provides a way to organize code into reusable modules and to include and use them in other parts of the application using the require()
function is used to include and run the code from a module in the current file, and the module.exports
and exports
objects are used to define and export the properties and methods of a module.
Node.js also provides a built-in module
object, which provides properties and methods for working with modules, such as module.id
and module.parent
.
Modules provide a way to organize and structure code, as well as a way to share and reuse code across an application. It also allows you to break down complex code into smaller, more manageable pieces.
// Exporting
module.exports = {
hello() {
console.log("Hello, World!");
},
};
// Importing
const myModule = require("./myModule");
// Exporting
export function hello() {
console.log("Hello, World!");
}
// Importing
import { hello } from "./myModule.js";