"In a world of spaghetti code and tangled logic, one guide rises above to bring clarity, structure, and true mastery."
Welcome to Superior JavaScript — your ultimate weapon to transcend the chaos of mediocre code and become a fearless JavaScript architect!
This isn’t just a guide; it’s a transformational journey.
From the messy underworld of procedural spaghetti code to the elegant heights of Object-Oriented Design, from demystifying prototypes to taming ES6+ tooling, this repository sharpens your knowledge into a razor-sharp edge.
| 🔥 Topics Mastered | 💼 What You'll Learn |
|---|---|
| 🧩 Object-Oriented Programming | Cut through spaghetti code with Classes & Objects |
| 🔐 Four Pillars of OOP | Encapsulation, Inheritance, Abstraction, Polymorphism |
| 🏗️ Constructors & Prototypes | Solid foundations to build scalable applications |
| 🎨 Advanced Patterns | Mixins, Factory Functions, Method Overriding & more |
| 🚀 ES6+ Tooling | Babel, Webpack, Vite essentials |
| 🗝️ Deep Dives | The secrets of this, Hoisting, Private Members, and Prototypes |
Object-Oriented Programming (OOP) is a programming approach that organizes code into objects, which represent real-world entities with data (attributes) and behaviors (methods). Instead of writing long procedural code, OOP structures programs using classes (blueprints) that create reusable objects. The four main principles of OOP—Encapsulation, Inheritance, Polymorphism, and Abstraction—help in keeping code modular, reusable, and easy to maintain. This approach solves issues like code duplication, security risks, and scalability by ensuring that different parts of a program are independent yet interconnected in a structured way.
- Reusability → Write once, reuse multiple times with classes & inheritance.
- Maintainability → Organized code is easier to debug & update.
- Scalability → Easily add new features without breaking old ones.
- Encapsulation → Keeps data secure by restricting access.
- Polymorphism → Use a single interface for different types of objects.
- Abstraction → Hides complex details, showing only necessary features.
Procedural programming often leads to spaghetti code, where functions and variables are scattered across the program without clear organization. As the codebase grows, it becomes hard to read, debug, and modify, since everything is tightly coupled and dependent on execution order. OOP solves this by structuring code into objects, encapsulating data and behaviors together, making it modular, reusable, and easier to maintain.
Object-Oriented Programming (OOP) is built on four fundamental principles that ensure code is structured, reusable, and maintainable. These principles—Encapsulation, Inheritance, Polymorphism, and Abstraction—help in organizing complex systems by breaking them into manageable, modular components. They enhance security, flexibility, and scalability, making OOP a preferred approach for large-scale software development.
Key Pillars of OOP:
- Encapsulation → Hides internal details and protects data using private variables and methods.
- Inheritance → Allows new classes to reuse properties and behaviors of existing classes.
- Polymorphism → Enables different objects to use the same method but behave differently.
- Abstraction → Hides complex implementation and exposes only essential details.
An object is a collection of key-value pairs that store related data and functions. Objects can be created using the Object constructor or object literals, which provide a simple and readable way to define objects using {}. Object literals are widely used because they make code concise, easy to read, and maintainable.
- Object → A data structure that holds properties (key-value pairs) and methods.
- Object Literal → A shorthand way to define objects using { key: value }.
- Readable & Simple → Object literals make object creation concise and clear.
- No Constructor Needed → Unlike new Object(), object literals require no extra syntax.
- Easily Expandable → You can dynamically add properties and methods.
A factory function is a function that returns objects without using the new keyword. It provides a flexible and reusable way to create multiple instances of an object, making it useful when dealing with dynamic object creation. Factory functions help avoid redundancy and make code more scalable and maintainable.
- Creates Objects → Returns a new object each time it's called.
- No new Keyword → Unlike constructors, it doesn’t require new.
- Encapsulation → Keeps object creation logic inside a function.
- Reusable → Easily generate multiple objects with different values.
function createCar(brand, model) {
return {
brand,
model,
drive() {
console.log(`${this.brand} ${this.model} is driving`);
}
};
}
const car1 = createCar("Tesla", "Model S");
const car2 = createCar("BMW", "X5");
car1.drive(); // Tesla Model S is driving
car2.drive(); // BMW X5 is drivingA constructor function is a special function used to create and initialize objects using the new keyword. It acts as a blueprint for creating multiple objects with the same properties and methods. Constructors help in making object creation structured, reusable, and scalable.
- Creates Objects → Used to instantiate multiple objects from a blueprint.
- Uses new Keyword → Must be called with new to return an object.
- Capitalized Name → By convention, constructor functions start with an uppercase letter.
- Reusability → Helps avoid redundant object definitions.
The constructor property is an inbuilt property of JavaScript objects that references the function that created the object. It helps identify the constructor used to create an object and can be useful for debugging or checking object types dynamically.
- References the Creator Function → Points to the function that constructed the object.
- Available on All Objects → Every object inherits the constructor property.
- Useful for Type Checking → Helps determine an object’s constructor.
- Can Be Overwritten → Can be manually changed, but rarely needed.
function Car(brand) {
this.brand = brand;
}
const myCar = new Car("Tesla");
console.log(myCar.constructor); // Outputs: [Function: Car]
console.log(myCar.constructor === Car); // trueIn JavaScript, functions are also objects, meaning they can have properties and methods like any other object. This allows functions to store data, be assigned to variables, passed as arguments, and even have their own methods. This flexibility makes JavaScript a powerful functional programming language.
- Functions Have Properties → Can store data like objects.
- Can Be Assigned → Functions can be stored in variables.
- Can Be Passed Around → Used as arguments in other functions (higher-order functions).
- Can Have Methods → Functions can have their own methods and properties.
function greet() {
console.log("Hello!");
}
greet.language = "English"; // Adding a property to the function
console.log(greet.language); // Outputs: English
greet(); // Outputs: Hello!Data types are categorized into value types (primitives) and reference types (objects). Value types store data directly in memory, while reference types store a reference (or address) to the actual data in memory. This affects how data is copied and compared.
Primitives are copied independently, while objects share the same reference!
- Value Types (Primitives) → Stored directly in memory.
- Reference Types (Objects) → Stored as a reference (memory address).
- Copying Behavior → Value types create independent copies, while reference types share the same memory location.
- Comparison → Value types compare actual values, while reference types compare memory addresses.
// Value Type (Primitive)
let a = 10;
let b = a; // Copying value
b = 20;
console.log(a); // 10 (unchanged)
console.log(b); // 20
// Reference Type (Object)
let obj1 = { name: "Alice" };
let obj2 = obj1; // Copying reference
obj2.name = "Bob";
console.log(obj1.name); // Bob (changed because obj1 and obj2 reference the same object)In JavaScript, objects are dynamic, meaning you can add or remove properties at any time. You can access and modify properties using dot notation (.) or bracket notation ([]). Bracket notation is useful when property names are dynamic or contain special characters.
- Adding Properties → Use object.property = value or object["property"] = value.
- Removing Properties → Use delete object.property.
- Bracket Notation → Needed for dynamic keys or when property names contain spaces or special characters.
let person = { name: "Alice" };
// Adding properties
person.age = 25; // Using dot notation
person["city"] = "New York"; // Using bracket notation
// Removing properties
delete person.age;
console.log(person); // { name: 'Alice', city: 'New York' }
// Using bracket notation with dynamic property name
let key = "email";
person[key] = "[email protected]"; // Adding dynamically
console.log(person.email); // Outputs: alice@example.comEnumerating properties means iterating over an object's keys to access its values. JavaScript provides multiple ways to do this, such as for...in loop, Object.keys(), Object.values(), and Object.entries(). These methods help when you need to process or display object properties dynamically.
- for...in Loop → Iterates over all enumerable properties of an object.
- Object.keys(obj) → Returns an array of property names (keys).
- Object.values(obj) → Returns an array of property values.
- Object.entries(obj) → Returns an array of key-value pairs.
let person = { name: "Alice", age: 25, city: "New York" };
// Using for...in loop
for (let key in person) {
console.log(key, person[key]); // name Alice, age 25, city New York
}
// Using Object.keys()
console.log(Object.keys(person)); // ["name", "age", "city"]
// Using Object.values()
console.log(Object.values(person)); // ["Alice", 25, "New York"]
// Using Object.entries()
console.log(Object.entries(person)); // [["name", "Alice"], ["age", 25], ["city", "New York"]]Abstraction is the concept of hiding unnecessary details and exposing only what's essential. In JavaScript, private properties and methods help achieve abstraction by restricting direct access to certain parts of an object. This improves security, maintainability, and prevents unintended modifications.
- Abstraction → Hides complex logic, exposing only what’s needed.
- Private Properties/Methods → Use # (private fields) or closures to keep data hidden.
- Encapsulation → Prevents direct modification of internal object states.
- Better Security → Protects sensitive data from external changes.
class Car {
#engineStatus = "off"; // Private property
start() {
this.#toggleEngine(true);
console.log("Car started");
}
#toggleEngine(status) { // Private method
this.#engineStatus = status ? "on" : "off";
}
}
const myCar = new Car();
myCar.start(); // Car started
console.log(myCar.#engineStatus); // ❌ Error: Private field not accessibleGetters and setters allow controlled access to object properties. A getter (get) retrieves a property value, while a setter (set) modifies it. They help encapsulate logic, validate data, and restrict direct property modification, making objects more secure and maintainable.
- Getter (get) → Allows reading a property like a variable.
- Setter (set) → Controls modification of a property with validation.
- Encapsulation → Hides direct property access and enforces rules.
- Improves Security → Prevents invalid data from being set.
class Person {
constructor(name) {
this._name = name; // Convention: Use _ for internal properties
}
get name() {
return this._name.toUpperCase(); // Custom logic while getting
}
set name(newName) {
if (newName.length > 2) {
this._name = newName; // Custom validation
} else {
console.log("Name must be at least 3 characters long");
}
}
}
const person = new Person("Alice");
console.log(person.name); // ALICE (Getter in action)
person.name = "Bo"; // ❌ Invalid (Setter validation)
person.name = "Bob"; // ✅ Valid update
console.log(person.name); // BOBPrototypes allow objects to inherit properties and methods from other objects. Every object in JavaScript has an internal link (proto) to its prototype, enabling reuse of functions and efficient memory usage. This forms the basis of prototypal inheritance.
Think prototype as parent
- Prototype Chain → Objects inherit properties/methods from their prototype.
- Memory Efficient → Shared methods are stored in one place, not copied.
- Custom Methods → You can add methods to an object’s prototype.
- Prototypal Inheritance → Objects inherit behavior from other objects.
function Person(name) {
this.name = name;
}
// Adding a method to the prototype
Person.prototype.greet = function () {
console.log(`Hello, my name is ${this.name}`);
};
const person1 = new Person("Alice");
const person2 = new Person("Bob");
person1.greet(); // Hello, my name is Alice
person2.greet(); // Hello, my name is Bob
console.log(person1.__proto__ === Person.prototype); // trueInheritance allows one object to inherit properties and methods from another, enabling code reuse and modular design. In JavaScript, inheritance is achieved using prototypes or the class keyword (extends). This makes it easier to create specialized objects based on existing ones.
- Prototypal Inheritance → Objects inherit from other objects via prototypes.
- Class-Based Inheritance → Uses class and extends to create child classes.
- Code Reusability → Avoids duplicate code by reusing parent functionality.
- Super Keyword → Calls methods from the parent class inside a child class.
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
bark() {
console.log(`${this.name} barks! 🐶`);
}
}
const dog = new Dog("Buddy");
dog.makeSound(); // Buddy makes a sound
dog.bark(); // Buddy barks! 🐶Prototypes allow objects to inherit properties and methods from other objects. Prototypical inheritance enables JavaScript objects to inherit from other objects directly, forming a prototype chain. This is more flexible than classical inheritance, as objects can dynamically inherit behavior without needing classes.
- Prototype Chain → Objects inherit from their prototype (proto).
- Shared Methods → Methods defined in a prototype are shared across instances.
- Efficient Memory Usage → Prevents duplication of functions in each object.
- Dynamic Inheritance → Objects can inherit behavior at runtime.
function Animal(name) {
this.name = name;
}
// Adding method to prototype
Animal.prototype.makeSound = function () {
console.log(`${this.name} makes a sound`);
};
function Dog(name, breed) {
Animal.call(this, name); // Inheriting properties
this.breed = breed;
}
// Inheriting methods
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function () {
console.log(`${this.name} barks! 🐶`);
};
const dog = new Dog("Buddy", "Labrador");
dog.makeSound(); // Buddy makes a sound
dog.bark(); // Buddy barks! 🐶
console.log(dog.__proto__ === Dog.prototype); // true
console.log(Dog.prototype.__proto__ === Animal.prototype); // trueMultilevel inheritance occurs when a class (or object) inherits from another class, which itself inherits from another. This creates a chain of inheritance, where properties and methods are passed down through multiple levels, promoting code reusability and modularity.
- Chained Inheritance → A child class inherits from a parent, which itself inherits from another class.
- Code Reusability → Avoids redundant code by passing down methods and properties.
- Prototype Chain → Each level in the hierarchy links to the previous one.
- Super Keyword → Used to call parent class methods in a subclass.
class Animal {
constructor(name) {
this.name = name;
}
makeSound() {
console.log(`${this.name} makes a sound`);
}
}
class Mammal extends Animal {
feedMilk() {
console.log(`${this.name} feeds milk`);
}
}
class Dog extends Mammal {
bark() {
console.log(`${this.name} barks! 🐶`);
}
}
const dog = new Dog("Buddy");
dog.makeSound(); // Buddy makes a sound
dog.feedMilk(); // Buddy feeds milk
dog.bark(); // Buddy barks! 🐶Property descriptors define the behavior of object properties in JavaScript. Every property has a descriptor that controls whether the property is writable, enumerable, and configurable. Using Object.defineProperty(), you can customize or lock down how properties behave, adding more control over your objects.
- Writable → Can the property's value be changed?
- Enumerable → Will the property show up in loops like for...in?
- Configurable → Can the property be deleted or modified?
- Control Access → Fine-tune object property behavior.
let person = {};
Object.defineProperty(person, "name", {
value: "Alice",
writable: false, // Can't change value
enumerable: true, // Will show in loops
configurable: false // Can't delete or redefine
});
console.log(person.name); // Alice
person.name = "Bob"; // ❌ Won't change because writable: false
console.log(person.name); // Alice
for (let key in person) {
console.log(key); // name (because enumerable: true)
}
delete person.name; // ❌ Won't delete because configurable: false
console.log(person.name); // Alicewhen you create an object using a constructor function, methods or properties shared across all instances are added to the constructor’s prototype. This allows all instances to access the same method without duplicating it in each object, making memory usage efficient and enabling prototypal inheritance.
- Constructor Function → Creates multiple similar objects.
- Prototype Property → Attach shared methods to save memory.
- Inheritance → All instances inherit methods from the prototype.
- Efficient → One method stored, shared by all instances.
function Person(name) {
this.name = name;
}
// Adding method to constructor's prototype
Person.prototype.greet = function () {
console.log(`Hello, I am ${this.name}`);
};
const person1 = new Person("Alice");
const person2 = new Person("Bob");
person1.greet(); // Hello, I am Alice
person2.greet(); // Hello, I am Bob
console.log(person1.__proto__ === Person.prototype); // trueInstance members are properties or methods defined inside the constructor function; each object instance gets its own copy. Prototype members, on the other hand, are defined on the constructor’s prototype and are shared across all instances, saving memory and enabling inheritance.
- Instance Members → Defined inside constructor, unique to each object.
- Prototype Members → Defined on prototype, shared by all instances.
- Memory Efficient → Prototype members avoid duplication.
- Customization → Use instance members for unique data, prototype members for common behavior.
function Person(name) {
// Instance member
this.name = name;
this.sayHello = function () {
console.log(`Hi, I am ${this.name}`);
};
}
// Prototype member
Person.prototype.greet = function () {
console.log(`Hello from ${this.name}`);
};
const person1 = new Person("Alice");
const person2 = new Person("Bob");
// Instance method (each has own copy)
person1.sayHello(); // Hi, I am Alice
person2.sayHello(); // Hi, I am Bob
// Prototype method (shared)
person1.greet(); // Hello from Alice
person2.greet(); // Hello from Bob
console.log(person1.sayHello === person2.sayHello); // false (separate copies)
console.log(person1.greet === person2.greet); // true (shared)you can iterate over both instance and prototype members using loops like for...in. However, instance members are directly present in the object, while prototype members are inherited via the prototype chain. To distinguish between them, you can use the hasOwnProperty() method, which checks if a property belongs directly to the instance.
- for...in Loop → Iterates over both instance and prototype members.
- hasOwnProperty() → Filters out prototype members, returning only instance members.
- Prototype Chain → Prototype members appear unless filtered.
- Control → Helps differentiate between own and inherited properties.
Extending built-in objects like Array, Object, or String might seem convenient, but it's discouraged. Modifying or adding methods to their prototypes can lead to unexpected behavior, conflicts with other libraries, and can break future updates, making your code less predictable and harder to maintain.
- Risk of Conflicts → Other libraries or future JavaScript versions might use the same method name.
- Unexpected Behavior → Can cause bugs when looping or using built-in functions.
- Hard to Maintain → Makes debugging and upgrading difficult.
- Best Practice → Create utility/helper functions instead of modifying built-ins.
// Bad practice: Extending built-in prototype
Array.prototype.sayHello = function () {
console.log("Hello from Array!");
};
const arr = [1, 2, 3];
arr.sayHello(); // Works, but risky!
for (let item in arr) {
console.log(item); // Will also include 'sayHello' - unexpected behavior!
}Prototypical inheritance allows one object to inherit properties and methods from another object through its prototype. Instead of classes, JavaScript uses a prototype chain where objects link to other objects, enabling flexible and dynamic inheritance. This promotes code reuse and shared behavior without duplicating functionality.
- Prototype Chain → Objects inherit from other objects' prototypes.
- Shared Methods → Reduces memory usage, as methods are not copied to each instance.
- Dynamic Inheritance → Objects can inherit behavior at runtime.
- Flexible Structure → No rigid class hierarchy needed.
const animal = {
eat: function () {
console.log(`${this.name} is eating`);
}
};
const dog = {
name: "Buddy"
};
// Setting prototype
Object.setPrototypeOf(dog, animal);
dog.eat(); // Buddy is eating
console.log(dog.__proto__ === animal); // trueYou can create your own prototypical inheritance by manually linking objects using Object.create() or by setting the prototype of one constructor function to an instance of another. This allows child objects to inherit properties and methods from parent objects, enabling code reuse and hierarchical relationships.
- Object.create() → Directly sets the prototype of a new object.
- Constructor Inheritance → Child constructor’s prototype points to parent’s prototype.
- Shared Behavior → Child objects can access parent methods.
- Flexible & Dynamic → Easy to modify or extend inheritance chains.
const animal = {
eat: function () {
console.log(`${this.name} is eating`);
}
};
const dog = Object.create(animal);
dog.name = "Max";
dog.eat(); // Max is eatingfunction Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats`);
};
function Dog(name) {
Animal.call(this, name); // Call parent constructor
}
Dog.prototype = Object.create(Animal.prototype); // Inherit prototype
Dog.prototype.constructor = Dog;
const dog1 = new Dog("Buddy");
dog1.eat(); // Buddy eatsWhen setting up prototypical inheritance using Object.create(), the child’s prototype inherits from the parent, but the constructor property of the child points to the parent constructor by default. To maintain clarity and avoid confusion, it’s important to reset the constructor property back to the child’s constructor.
- Prototype Chain Issue → Child's prototype constructor points to parent constructor.
- Why Reset? → Keeps the correct reference to child’s constructor.
- Maintains Clarity → Helps in debugging and object identification.
- Simple Fix → Use Child.prototype.constructor = Child;
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats`);
};
function Dog(name) {
Animal.call(this, name);
}
// Inherit from Animal
Dog.prototype = Object.create(Animal.prototype);
// Resetting constructor
Dog.prototype.constructor = Dog;
const dog1 = new Dog("Buddy");
console.log(dog1.constructor === Dog); // true
console.log(dog1.constructor); // Outputs: Dog functionwhen creating inheritance using constructor functions, the child constructor needs to call the parent (super) constructor to properly initialize the inherited properties. This is done using Parent.call(this, ...) inside the child constructor. It ensures that the parent’s initialization logic runs for each child instance.
- Parent Initialization → Use Parent.call(this, ...) to inherit properties.
- Avoid Duplication → Ensures parent setup runs in child.
- Proper Context → this refers to the child instance during the call.
- Required for Inherited Properties → Without it, child lacks parent properties.
function Animal(name) {
this.name = name;
}
function Dog(name, breed) {
Animal.call(this, name); // Call super constructor
this.breed = breed;
}
const dog1 = new Dog("Buddy", "Labrador");
console.log(dog1.name); // Buddy
console.log(dog1.breed); // LabradorIntermediate function inheritance is a technique used to set up inheritance between two constructor functions without directly modifying the prototype chain, helping to avoid unwanted side effects. It introduces a temporary constructor function as a bridge to correctly link the prototypes while keeping the child constructor’s prototype clean and safe.
- Temporary Constructor Function → Acts as a middle layer.
- Avoids Direct Modification → Prevents altering parent prototypes directly.
- Safe & Clean Inheritance → Keeps prototype chains intact.
- Maintains Constructor Reference → Child constructor remains correct.
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats`);
};
function Dog(name) {
Animal.call(this, name);
}
// Intermediate function
function Temp() {}
Temp.prototype = Animal.prototype;
// Set up inheritance
Dog.prototype = new Temp();
Dog.prototype.constructor = Dog;
const dog1 = new Dog("Buddy");
dog1.eat(); // Buddy eats
console.log(dog1.constructor === Dog); // trueMethod overriding occurs when a child object or subclass redefines a method inherited from its parent, providing a more specific or customized implementation. It’s a powerful way to extend or modify behavior while keeping the base functionality available if needed.
- Same Method Name → Child provides its own version of the method.
- Custom Behavior → Allows tailoring inherited methods.
- Can Still Call Parent Method → Use Parent.prototype.method.call(this) if needed.
- Polymorphism → Enables flexible and dynamic behavior changes.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} makes a sound`);
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
// Overriding speak method
Dog.prototype.speak = function () {
console.log(`${this.name} barks`);
};
const dog1 = new Dog("Buddy");
dog1.speak(); // Buddy barksPolymorphism means "many forms" — it allows different objects to be treated as if they are instances of the same parent, yet each can perform behavior differently. It’s achieved by having different classes or objects implement the same method name but with different functionality, allowing flexible and dynamic code without worrying about exact object types.
- Same Interface, Different Behavior → Different objects share method names but behave differently.
- Method Overriding Enables It → Each subclass provides its own implementation.
- Simplifies Code → Write general code without worrying about specific object types.
- Supports Extensibility → Easily add new object types without changing existing code.
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function () {
console.log(`${this.name} makes a sound`);
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function () {
console.log(`${this.name} barks`);
};
function Cat(name) {
Animal.call(this, name);
}
Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;
Cat.prototype.speak = function () {
console.log(`${this.name} meows`);
};
// Polymorphism in action
const animals = [new Dog("Buddy"), new Cat("Kitty")];
animals.forEach(animal => animal.speak());
// Output:
// Buddy barks
// Kitty meowsInheritance is best used when multiple objects share common properties or behavior, and you want to avoid code duplication by moving shared logic to a parent object or class. It helps create a clear hierarchical relationship, making the code organized, reusable, and easier to maintain.
- Shared Behavior → When multiple objects need the same methods or properties.
- Avoid Duplication → Move common logic to a parent object.
- Logical Hierarchies → Use when there’s a natural “is-a” relationship (e.g., Dog is an Animal).
- Code Maintenance → Changes in the parent reflect across all child objects.
- Extensibility → Easily add new child types without rewriting common code.
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function () {
console.log(`${this.name} eats`);
};
function Dog(name) {
Animal.call(this, name);
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
const dog1 = new Dog("Buddy");
dog1.eat(); // Buddy eatsMixins are a way to reuse reusable chunks of functionality across multiple unrelated objects without using inheritance. Instead of forming a parent-child relationship, mixins allow you to copy methods or properties into any object or class, providing flexibility and avoiding rigid hierarchies. It’s great when you want multiple behaviors in different objects that don't fit into a single inheritance chain.
- No Hierarchy Needed → Use when inheritance doesn't make sense.
- Behavior Sharing → Share methods/properties across different objects.
- Flexible & Modular → Easily compose behaviors without tight coupling.
- Avoids Deep Inheritance Chains → Keeps code simple and flat.
- Implemented via Object.assign() or functions.
let canEat = {
eat: function() {
console.log("Eating...");
}
};
let canWalk = {
walk: function() {
console.log("Walking...");
}
};
function Person(name) {
this.name = name;
}
// Apply mixins
Object.assign(Person.prototype, canEat, canWalk);
const person1 = new Person("John");
person1.eat(); // Eating...
person1.walk(); // Walking...ES6 classes provide a cleaner, more readable syntax for creating objects and handling inheritance in JavaScript. Under the hood, they still use prototypal inheritance, but with a structure similar to traditional class-based languages like Java or C++. This makes code easier to understand and maintain, while still supporting all the powerful OOP features like constructors, methods, and inheritance.
- Cleaner Syntax → More readable and organized than constructor functions.
- Built-in Inheritance Support → Use extends and super() for easy inheritance.
- Supports OOP Principles → Encapsulation, inheritance, and polymorphism.
- Syntactic Sugar → Still uses prototypes internally but hides complexity.
- Easier Method Definitions → No need to manually assign methods to prototype.
class Animal {
constructor(name) {
this.name = name;
}
eat() {
console.log(`${this.name} eats`);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // Call parent constructor
this.breed = breed;
}
bark() {
console.log(`${this.name} barks`);
}
}
const dog1 = new Dog("Buddy", "Labrador");
dog1.eat(); // Buddy eats
dog1.bark(); // Buddy barksHoisting is JavaScript’s behavior of moving declarations (variables & functions) to the top of their scope before code execution. This means you can reference functions and variables before they are declared in the code. However, only the declarations are hoisted, not the initializations. It often leads to confusion if not properly understood.
- Variable & Function Declarations Move Up → Only declarations, not initial values.
- var Variables Hoisted & Undefined Initially → They exist but have undefined until assigned.
- let and const Are Hoisted But in TDZ → Can't be accessed before declaration (Temporal Dead Zone).
- Function Declarations Fully Hoisted → Can call them before declaration.
- Helps Avoid Reference Errors, But Can Cause Bugs If Misused.
console.log(x); // undefined
var x = 5;
sayHello(); // "Hello!"
function sayHello() {
console.log("Hello!");
}
// let & const example (Temporal Dead Zone)
console.log(y); // ReferenceError
let y = 10;Static methods are functions defined on a class itself, not on instances of the class. You call them directly on the class, not on an object created from the class. They're typically used for utility or helper functions that are related to the class but don’t need access to instance-specific data.
- Belong to Class, Not Instance → Called on the class, not individual objects.
- No Access to this Instance Properties → Meant for general-purpose tasks.
- Used for Utility Functions → Like calculations, validations, etc.
- Defined Using static Keyword.
class MathHelper {
static add(a, b) {
return a + b;
}
}
console.log(MathHelper.add(3, 4)); // 7
const obj = new MathHelper();
console.log(obj.add); // undefined (cannot call static method on instance)this Keyword in JavaScript: The this keyword refers to the object that is executing the current function. Its value changes depending on how and where a function is called—it can point to different objects like the global object, a specific object, or undefined (in strict mode). Understanding this is crucial to control object behavior and method execution in JavaScript.
- In Methods → this refers to the object owning the method.
- In Functions (non-strict mode) → this refers to the global object (window in browsers).
- In Strict Mode → this is undefined in regular functions.
- With new Keyword → this refers to the new object being created.
- Arrow Functions → Do not have their own this; they inherit from the surrounding scope.
const person = {
name: "John",
greet: function () {
console.log(`Hello, my name is ${this.name}`);
}
};
person.greet(); // Hello, my name is John
function show() {
console.log(this); // In non-strict mode: window/global object
}
show();
const obj = new person.greet.constructor("name", "console.log(this.name);");
const newObj = { name: "Jane" };
obj.call(newObj); // JaneSymbols in JavaScript provide a way to create unique, hidden property keys that can't be accidentally accessed or overwritten. By using Symbols, developers can simulate private members in objects, keeping certain properties or methods hidden from outside access. Though not truly private (like in some languages), they prevent unintentional access and naming collisions.
- Symbols Create Unique Keys → Avoid property name conflicts.
- Not Accessible via Regular Property Names → Hidden unless you have the Symbol reference.
- Great for Simulating Private Members → Keeps implementation details safe.
- Non-Enumerable → They won’t show up in for...in loops or Object.keys().
const _salary = Symbol('salary');
class Employee {
constructor(name, salary) {
this.name = name;
this[_salary] = salary;
}
getSalary() {
return this[_salary];
}
}
const emp = new Employee("John", 5000);
console.log(emp.name); // John
console.log(emp.getSalary()); // 5000
console.log(emp.salary); // undefined (hidden)WeakMaps offer a powerful way to implement truly private members in JavaScript. A WeakMap holds key-value pairs where the key is an object and the value can be anything (like private data). Since WeakMaps are not directly accessible and do not expose their contents, they ensure full encapsulation and privacy of sensitive data.
- Truly Private → No way to access the private data outside the class.
- Keys Are Objects Only → WeakMap keys are object references.
- Garbage Collected → No memory leaks; entries are removed when the object is no longer referenced.
- Encapsulation → Keeps internal details hidden and secure.
- Perfect for Sensitive Data Handling.
const _privateData = new WeakMap();
class Person {
constructor(name, age) {
this.name = name;
_privateData.set(this, { age: age });
}
getAge() {
return _privateData.get(this).age;
}
}
const p1 = new Person("Alice", 30);
console.log(p1.name); // Alice
console.log(p1.getAge()); // 30
console.log(p1.age); // undefined (hidden)Getters and Setters are special methods in JavaScript used to control access to an object's properties. A getter retrieves (gets) the value of a property, while a setter sets or updates its value. They allow you to add custom logic when reading or modifying properties, ensuring validation, encapsulation, or computed properties without directly exposing the internal data.
- Getter (get) → Used to return or compute property values.
- Setter (set) → Used to validate or modify property values before setting them.
- Encapsulation → Control internal property access safely.
- Cleaner Syntax → Access like normal properties, but with added logic.
- Useful for Validation & Computation.
class Person {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(newName) {
if (newName.length > 0) {
this._name = newName;
} else {
console.log("Name cannot be empty!");
}
}
}
const p1 = new Person("Alice");
console.log(p1.name); // Alice
p1.name = "Bob";
console.log(p1.name); // Bob
p1.name = ""; // Name cannot be empty!Inheritance in ES6 is achieved using the class and extends keywords. It allows a class (child) to inherit properties and methods from another class (parent), promoting code reuse, modularity, and hierarchical relationships between classes. This makes it easier to build complex systems with shared behaviors while allowing specialized behavior in child classes.
- extends Keyword → Used to create a child class from a parent class.
- super() Function → Calls the parent’s constructor inside the child’s constructor.
- Code Reusability → Avoid duplication, reuse common logic.
- Supports Method Overriding → Customize or enhance parent methods.
- Encourages Cleaner & Organized Code.
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // Call parent constructor
}
speak() {
console.log(`${this.name} barks`);
}
}
const dog = new Dog("Buddy");
dog.speak(); // Buddy barksMethod Overriding in ES6 allows a child class to provide its own implementation of a method that is already defined in the parent class. This helps in customizing or extending the behavior of inherited methods, enabling specialized behavior while still reusing the parent class structure.
- Same Method Name → Child class redefines a method from the parent.
- Allows Custom Behavior → Child class can modify or enhance the method logic.
- Parent’s Method Accessible via super → Call parent’s version if needed.
- Supports Polymorphism → Treat child and parent objects uniformly.
- Keeps Code Flexible & DRY.
class Animal {
speak() {
console.log("Animal makes a sound");
}
}
class Dog extends Animal {
speak() {
console.log("Dog barks"); // Overridden method
}
}
const dog = new Dog();
dog.speak(); // Dog barksES6 Tooling refers to the set of modern tools and technologies that help developers write, compile, and manage ES6+ (modern JavaScript) code efficiently. Since not all browsers initially supported ES6 features, tools like transpilers, bundlers, and linters ensure your code works across environments, stays optimized, and follows best practices.
- Babel (Transpiler) → Converts ES6+ code to older ES5 for browser compatibility.
- Webpack/Rollup (Bundlers) → Combines multiple JS files/modules into one optimized bundle.
- ESLint (Linter) → Checks code for syntax errors and enforces coding standards.
- Prettier (Formatter) → Automatically formats code for consistent style.
- NPM/Yarn → Manage project dependencies and packages.
ES6 Modules allow you to break your JavaScript code into smaller, reusable pieces (modules) and import/export them as needed. This helps in organizing code, avoiding global scope pollution, and making maintenance easier. Each module has its own scope, promoting clean, modular design.
- export Keyword → Exports functions, variables, or classes from a module.
- import Keyword → Imports exported members from other modules.
- Scoped → Each module has its own scope, avoiding global namespace conflicts.
- Supports Default & Named Exports → Flexible exporting options.
- Improves Code Reusability & Maintainability.
// file: math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14;
// file: main.js
import { add, PI } from './math.js';
console.log(add(2, 3)); // 5
console.log(PI); // 3.14CommonJS Modules are the standard module system used primarily in Node.js environments. They allow developers to export and import functionality using module.exports and require(). Unlike ES6 modules, CommonJS loads modules synchronously, making it ideal for server-side code.
- module.exports → Used to export functions, objects, or variables.
- require() → Used to import modules.
- Synchronous Loading → Loads modules one at a time (good for servers).
- Node.js Default Module System → Widely used in backend JavaScript.
- Simpler Syntax → Straightforward and easy to implement.
// file: math.js
function add(a, b) {
return a + b;
}
module.exports = { add };
// file: app.js
const math = require('./math');
console.log(math.add(2, 3)); // 5Babel is a popular JavaScript transpiler that converts modern ES6+ code into backward-compatible JavaScript (ES5) so that it can run smoothly on older browsers and environments. It allows developers to use the latest JS features without worrying about browser support, making development more future-proof.
- Transpiles ES6+ → ES5 → Ensures compatibility across all browsers.
- Supports JSX, TypeScript, Flow → Works well with React, TypeScript, etc.
- Highly Configurable → Use plugins & presets based on project needs.
- Works with Bundlers → Often used with Webpack or Rollup.
- Future-Proof Coding → Write modern syntax without compatibility issues.
# Install Babel and presets
npm install --save-dev @babel/core @babel/cli @babel/preset-env
# Create a config file (babel.config.json)
{
"presets": ["@babel/preset-env"]
}
# Run Babel to transpile code
npx babel src --out-dir dist// ES6 INPUT
// src/app.js
const greet = () => console.log("Hello, ES6!");
// ES5 OUTPUT
"use strict";
var greet = function greet() {
return console.log("Hello, ES6!");
};Webpack is a powerful module bundler for JavaScript applications. It takes multiple files (JS, CSS, images, etc.) and bundles them into a single optimized file or smaller chunks. This helps in managing dependencies, improving load times, and making the development process more efficient.
- Bundles Multiple Files → Combines JS, CSS, images, etc., into one or more files.
- Supports Loaders → Handles different file types (like Babel for JS, CSS loaders).
- Plugins for Optimization → Minification, caching, and performance tweaks.
- Development Server (Webpack Dev Server) → For live reloading during development.
- Modular Code Structure → Helps organize large codebases.
Vite is a modern frontend build tool that offers a fast and lightweight development experience. Unlike older bundlers (like Webpack), Vite uses native ES modules (ESM) and leverages the browser’s capabilities for faster loading during development. It also provides efficient production builds with minimal configuration.
- Super Fast Startup → Instant dev server with near-zero config.
- Native ESM Support → No bundling during development = faster refresh.
- Optimized Production Builds → Uses Rollup internally for final build.
- Hot Module Replacement (HMR) → Instant updates without full reloads.
- Support for Modern Frameworks → Works seamlessly with React, Vue, Svelte, etc.