Technology Aug 23, 2026 · 15 min read

# Week 4 - Task 1: Understanding JavaScript Core Concepts

JavaScript Prototypes, Inheritance, Classes, and Encapsulation When I first started learning JavaScript objects, prototype, Object.create(), class, inheritance, getters, setters, and private fields felt like completely different topics. After working through them, I started seeing that m...

DE
DEV Community
by koushikmaya
# Week 4 - Task 1: Understanding JavaScript Core Concepts

JavaScript Prototypes, Inheritance, Classes, and Encapsulation

When I first started learning JavaScript objects, prototype, Object.create(), class, inheritance, getters, setters, and private fields felt like completely different topics.

After working through them, I started seeing that most of them are connected through one main idea:

JavaScript objects use prototypes underneath.

This is my beginner-friendly understanding of the topic, with the examples I used while learning.

What I am learning

In this note, I am connecting:

  • Prototype and prototype chain
  • Object.create()
  • Constructor functions
  • Constructor function + prototype
  • Inheritance
  • class and why it works with prototypes
  • extends and super
  • Static members
  • Getters and setters
  • Encapsulation with closures
  • Private fields using #
  • How all these concepts fit together

1. First: What is a Prototype?

A prototype is another object that JavaScript can look at when it cannot find a property or method directly on the current object.

A simple example:

const animal = {
    eat() {
        console.log("Eating");
    }
};

const dog = Object.create(animal);

dog.bark = function () {
    console.log("Barking");
};

dog.bark();
dog.eat();

Output:

Barking
Eating

The important thing is that eat() is not actually inside dog.

JavaScript finds it through dog's prototype.

dog
 ↓
animal
 ↓
Object.prototype
 ↓
null

So I can think of a prototype as:

"If you don't have something, JavaScript can look here next."

2. Prototype Chain

The prototype chain is simply the path JavaScript follows when looking for a property or method.

For example:

const person = {
    name: "Koushik"
};

console.log(person.toString());

I never created toString() inside person.

So how does it work?

JavaScript searches:

person
 ↓
Object.prototype
 ↓
null

toString() is found on Object.prototype.

Property lookup in simple steps

When I write:

object.someProperty

JavaScript roughly does this:

  1. Look inside object.
  2. If it finds the property, use it.
  3. If not, look at the object's prototype.
  4. Keep moving up the prototype chain.
  5. If it reaches null, the property was not found.

For example:

const animal = {
    eat() {
        console.log("Eating");
    }
};

const dog = Object.create(animal);

dog.eat();

The search is:

dog
 ↓
animal          ← eat() found here
 ↓
Object.prototype
 ↓
null

3. prototype vs __proto__

These names confused me at first, so I keep this distinction clear.

prototype

prototype is commonly seen with constructor functions and classes.

Example:

function Person(name) {
    this.name = name;
}

Person.prototype.sayHello = function () {
    console.log(`Hello ${this.name}`);
};

const person = new Person("Koushik");

person.sayHello();

The object created by new Person() can find sayHello() through:

Person.prototype

The chain is:

person
 ↓
Person.prototype
 ↓
Object.prototype
 ↓
null

__proto__

__proto__ gives access to an object's actual prototype.

console.log(person.__proto__ === Person.prototype);

Output:

true

For modern code, I should prefer:

Object.getPrototypeOf(person);

instead of directly using __proto__.

4. Object.create()

Object.create() creates a new object and lets me choose its prototype.

Syntax:

Object.create(prototype);

Example:

const animal = {
    eat() {
        console.log("Eating");
    }
};

const dog = Object.create(animal);

dog.bark = function () {
    console.log("Barking");
};

dog.eat();
dog.bark();

The relationship is:

dog
 ↓
animal

This is the simplest form of object-to-object inheritance.

My simple way of remembering it

const child = Object.create(parent);

means:

"Create a new object whose prototype is parent."

5. Object.create() Inheritance

This is the first inheritance pattern I learned.

const obj = {
    name: "Koushik",
    age: 25,

    getinfo() {
        console.log(`My name is ${this.name}`);
    }
};

const obj2 = Object.create(obj);

obj2.getage = function () {
    console.log(`My age is ${this.age}`);
};

obj2.getinfo();
obj2.getage();

Here:

obj2
 ↓
obj

obj2 can use getinfo() because JavaScript finds it in obj.

So this is:

Object-to-object inheritance.

This is simple and useful when I already have an object that I want another object to be based on.

6. Constructor Functions

Before understanding inheritance with constructor functions, I first need to understand what a constructor function does.

Example:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

I can think of this as a blueprint.

Then:

const bike1 = new Bike("Yamaha", "Blue", 1000);

creates an object.

Conceptually:

bike1
├── company → "Yamaha"
├── colour  → "Blue"
└── cc      → 1000

The new keyword is important because it creates a new object and connects that object to Bike.prototype.

7. Constructor Function + Prototype

Now I can add methods to the prototype.

My example:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

Bike.prototype.fun = function () {
    console.log(
        "Hi, is this " +
        this.company + " " +
        this.colour +
        " colour bike. This looks good. Is this " +
        this.cc + "?"
    );
};

const bike1 = new Bike("Yamaha", "Blue", 1000);

bike1.fun();

The important thing is that fun() is on:

Bike.prototype

not copied directly into every object.

The chain is:

bike1
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null

When I call:

bike1.fun();

JavaScript looks for fun().

It checks:

bike1                 → not found
Bike.prototype        → found

So the method runs.

8. Important: Constructor + Prototype Is Not Automatically Inheritance

This was an important distinction for me.

If I write:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

Bike.prototype.fun = function () {
    console.log("Bike method");
};

const bike1 = new Bike("Yamaha", "Blue", 1000);

I have:

  • A constructor function
  • A prototype
  • An instance

But I do not yet have a parent-child inheritance relationship.

The chain is only:

bike1
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null

Actual constructor-function inheritance needs a second constructor.

9. Constructor Function + Prototype Inheritance

Now I can make:

Bike
 ↓
SuperBike

Here is my example:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

Bike.prototype.fun = function () {
    console.log(
        "Hi, is this " +
        this.company + " " +
        this.colour +
        " colour bike. This looks good. Is this " +
        this.cc + "?"
    );
};


function SuperBike(company, colour, cc, topspeed) {

    Bike.call(this, company, colour, cc);

    this.topspeed = topspeed;
}


// Actual prototype inheritance
SuperBike.prototype = Object.create(Bike.prototype);


// SuperBike's own method
SuperBike.prototype.showSpeed = function () {
    console.log(
        this.company +
        " has a top speed of " +
        this.topspeed +
        " km/h"
    );
};


const bike1 = new SuperBike(
    "Yamaha",
    "Blue",
    1000,
    300
);

bike1.fun();
bike1.showSpeed();

Where does inheritance actually happen?

This line:

SuperBike.prototype = Object.create(Bike.prototype);

is the important inheritance line.

It creates:

bike1
 ↓
SuperBike.prototype
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null

So:

bike1.fun();

works because JavaScript eventually finds fun() in Bike.prototype.

10. Why Bike.call()?

This line:

Bike.call(this, company, colour, cc);

handles the parent's properties.

The parent constructor has:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

When Bike.call(this, ...) runs inside SuperBike, this refers to the new SuperBike object.

So it effectively sets:

this.company = company;
this.colour = colour;
this.cc = cc;

Then SuperBike adds:

this.topspeed = topspeed;

So I remember the pattern like this:

Bike.call(...)
      ↓
Get parent's properties

Object.create(Bike.prototype)
      ↓
Get parent's prototype methods

11. Object.create() vs Constructor Inheritance

This was one of the things I wanted to understand clearly.

Object.create()

const child = Object.create(parent);

This is:

"Make this object inherit from that object."

Simple object-to-object inheritance.

Constructor + prototype

function Parent() {}

Parent.prototype.method = function () {};

const child = new Parent();

This is mainly:

"Create many instances from a constructor and share methods through the prototype."

It is not automatically parent-child inheritance.

Actual constructor inheritance

Child.prototype = Object.create(Parent.prototype);

Now I have:

Child
 ↓
Parent

So I do not need to think of Object.create() and constructor inheritance as unrelated topics.

Object.create() is actually part of the old-style inheritance pattern.

12. Classes

Modern JavaScript gives us class.

Example:

class Bike {
    constructor(company, colour, cc) {
        this.company = company;
        this.colour = colour;
        this.cc = cc;
    }

    fun() {
        console.log(
            `This is a ${this.colour} ${this.company} bike`
        );
    }
}

const bike1 = new Bike("Yamaha", "Blue", 1000);

bike1.fun();

This looks very different from:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}

Bike.prototype.fun = function () {
    // ...
};

But the important thing is:

Classes still use prototypes underneath.

The class method is placed on:

Bike.prototype

rather than copied separately into every instance.

So:

class syntax
     ↓
prototype system underneath

This is why classes are often described as syntactic sugar over the prototype-based system.

13. Class Inheritance with extends

With classes, inheritance becomes much cleaner.

class Bike {
    constructor(company, colour) {
        this.company = company;
        this.colour = colour;
    }

    ride() {
        console.log(`${this.company} is riding`);
    }
}

class SuperBike extends Bike {
    showSpeed() {
        console.log("Top speed is 300 km/h");
    }
}

const bike = new SuperBike("Yamaha", "Blue");

bike.ride();
bike.showSpeed();

The inheritance chain is approximately:

bike
 ↓
SuperBike.prototype
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null

So bike.ride() works even though ride() is defined in Bike.

14. super

super is used when I want to work with the parent class.

Example:

class Bike {
    constructor(company, colour) {
        this.company = company;
        this.colour = colour;
    }
}

class SuperBike extends Bike {
    constructor(company, colour, speed) {
        super(company, colour);
        this.speed = speed;
    }
}

This:

super(company, colour);

calls the parent constructor.

It is similar in purpose to:

Bike.call(this, company, colour);

in constructor-function inheritance.

One important rule:

In a derived class constructor, super() must be called before using this.

15. Static Members

A normal method belongs to an instance:

class Bike {
    show() {
        console.log("Bike");
    }
}

const bike = new Bike();

bike.show();

A static method belongs to the class itself:

class Bike {
    static info() {
        console.log("Bike information");
    }
}

Bike.info();

I should not do:

const bike = new Bike();

bike.info(); // ❌

because info() is static.

Static property

class Bike {
    static wheels = 2;
}

console.log(Bike.wheels);

The important difference is:

Normal method
     ↓
instance

Static method
     ↓
class itself

16. Static with Constructor Functions

I can also create a static-like member with a constructor function.

function Bike(company) {
    this.company = company;
}

Bike.info = function () {
    console.log("Bike information");
};

Bike.info();

Here info is attached directly to Bike.

I don't use:

const bike = new Bike("Yamaha");

bike.info(); // ❌

The main idea is:

Bike.info()
    ↓
belongs to Bike itself

bike.info()
    ↓
looks for an instance method

For modern JavaScript, I will usually see the static keyword with classes.

17. Getters

A getter lets me read a method like a property.

Without a getter:

class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    getFullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

const user = new User("Koushik", "Maya");

console.log(user.getFullName());

I have to use:

user.getFullName();

With a getter:

class User {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }
}

const user = new User("Koushik", "Maya");

console.log(user.fullName);

Now I use:

user.fullName

not:

user.fullName()

The simple meaning is:

get → when I READ the property, run this code

18. Getters and Setters — My Age Example

This is the example that helped me understand getters and setters.

class Person {
    constructor(name, age) {
        this.name = name;
        this._age = age;
    }

    get age() {
        return this._age;
    }

    set age(newAge) {
        if (newAge < 0) {
            console.log("Age cannot be negative");
            return;
        }

        this._age = newAge;
    }
}

const p1 = new Person("Koushik", 25);

console.log(p1.age);

p1.age = 26;

console.log(p1.age);

Output:

25
26

What is happening?

When I write:

console.log(p1.age);

I am reading age.

So the getter runs:

get age() {
    return this._age;
}

It returns:

25

Then when I write:

p1.age = 26;

I am setting/changing age.

So the setter runs:

set age(newAge) {

Here:

newAge = 26

The condition:

if (newAge < 0)

is false.

So:

this._age = newAge;

changes the stored value to:

_age = 26

When I read it again:

console.log(p1.age);

the getter returns 26.

19. Why _age?

I use:

this._age

as the internal storage.

The _ is only a convention. It does not make the property private.

So this is still possible:

console.log(p1._age);

The idea is:

_age
 ↓
internal storage

age
 ↓
getter/setter interface

This also prevents a common problem.

If I wrote:

set age(newAge) {
    this.age = newAge;
}

the setter would call itself again and again.

So instead I store the actual value in:

this._age

20. Setter = Validation and Controlled Changes

A setter becomes especially useful when I need to control what values are allowed.

For example:

class BankAccount {
    constructor(balance) {
        this._balance = balance;
    }

    get balance() {
        return this._balance;
    }

    set balance(amount) {
        if (amount < 0) {
            console.log("Balance cannot be negative");
            return;
        }

        this._balance = amount;
    }
}

Now:

account.balance = 5000;

is allowed.

But:

account.balance = -5000;

is rejected.

So:

Getter
  ↓
Controls reading

Setter
  ↓
Controls changing

21. Encapsulation

Encapsulation means keeping internal implementation details controlled instead of exposing everything directly.

For example, this is very open:

class BankAccount {
    constructor(balance) {
        this.balance = balance;
    }
}

Someone can do:

account.balance = 999999;

There is no validation.

JavaScript gives me different ways to keep internal state controlled.

The two important approaches here are:

  1. Closures
  2. Private class fields using #

22. Encapsulation with Closures

A closure allows a function to remember variables from its outer scope.

Example:

function BankAccount(initialBalance) {

    let balance = initialBalance;

    return {
        deposit(amount) {
            if (amount <= 0) {
                throw new Error("Invalid amount");
            }

            balance += amount;
        },

        withdraw(amount) {
            if (amount > balance) {
                throw new Error("Insufficient balance");
            }

            balance -= amount;
        },

        getBalance() {
            return balance;
        }
    };
}

Create an account:

const account = BankAccount(1000);

Use it:

account.deposit(500);

console.log(account.getBalance());

Output:

1500

But:

console.log(account.balance);

returns:

undefined

Why?

Because balance is not a property of account.

It is a variable inside the BankAccount() function.

The returned functions remember it.

Conceptually:

BankAccount()
     │
     ├── balance = 1000   ← hidden
     │
     ├── deposit()
     ├── withdraw()
     └── getBalance()
              │
              └── can access balance

This is closure-based encapsulation.

23. Private Fields with #

Modern JavaScript gives us actual private class fields.

class BankAccount {
    #balance;

    constructor(initialBalance) {
        this.#balance = initialBalance;
    }

    deposit(amount) {
        this.#balance += amount;
    }

    getBalance() {
        return this.#balance;
    }
}

Use it:

const account = new BankAccount(1000);

account.deposit(500);

console.log(account.getBalance());

Output:

1500

But this is not allowed:

console.log(account.#balance);

#balance is actually private to the class.

24. _field vs #field

This distinction is very important.

this._balance

means:

"I intend this to be an internal property."

But JavaScript does not enforce that.

Someone can still do:

account._balance;

Whereas:

this.#balance

is a real private field.

So:

_balance
   ↓
Convention only

#balance
   ↓
Actual JavaScript private field

25. Private Methods

Methods can also be private.

class BankAccount {
    #balance = 1000;

    #validateAmount(amount) {
        return amount > 0;
    }

    deposit(amount) {
        if (!this.#validateAmount(amount)) {
            throw new Error("Invalid amount");
        }

        this.#balance += amount;
    }
}

This method:

#validateAmount()

can only be used inside the class.

Outside code cannot call:

account.#validateAmount(100);

26. Closures vs Private Fields

Both can hide internal state.

Closure

function Counter() {
    let count = 0;

    return {
        increment() {
            count++;
        },

        getCount() {
            return count;
        }
    };
}

Here count is hidden inside the function scope.

Private field

class Counter {
    #count = 0;

    increment() {
        this.#count++;
    }

    getCount() {
        return this.#count;
    }
}

Here #count is private because JavaScript enforces the private-field rule.

My simple distinction:

Closure
   ↓
private variable in lexical scope

#field
   ↓
JavaScript-enforced private class field

27. One Complete Example

Now I can combine the concepts:

  • Class
  • Inheritance
  • static
  • Getter
  • Setter
  • Private field
class Person {

    static species = "Human";

    #age;

    constructor(name, age) {
        this.name = name;
        this.#age = age;
    }

    get age() {
        return this.#age;
    }

    set age(value) {
        if (value < 0) {
            throw new Error("Age cannot be negative");
        }

        this.#age = value;
    }

    introduce() {
        console.log(
            `My name is ${this.name} and I am ${this.#age} years old.`
        );
    }
}


class Student extends Person {

    constructor(name, age, course) {
        super(name, age);
        this.course = course;
    }

    study() {
        console.log(
            `${this.name} is studying ${this.course}`
        );
    }
}


const student = new Student(
    "Koushik",
    22,
    "JavaScript"
);

student.introduce();
student.study();

console.log(student.age);

student.age = 23;

console.log(student.age);

console.log(Person.species);

Output:

My name is Koushik and I am 22 years old.
Koushik is studying JavaScript
22
23
Human

28. Understanding the Prototype Chain in This Example

When I write:

const student = new Student(
    "Koushik",
    22,
    "JavaScript"
);

the object is connected to prototypes roughly like this:

student
 ↓
Student.prototype
 ↓
Person.prototype
 ↓
Object.prototype
 ↓
null

If I call:

student.study();

JavaScript finds study() on:

Student.prototype

If I call:

student.introduce();

JavaScript searches:

student
 ↓
Student.prototype
 ↓
Person.prototype       ← introduce() found

This is what is happening underneath class inheritance.

29. Where Does #age Fit?

The private field is used inside the getter and setter.

Getter:

get age() {
    return this.#age;
}

So:

console.log(student.age);

flows like:

student.age
    ↓
get age()
    ↓
this.#age
    ↓
private value

When I write:

student.age = 23;

the setter runs:

student.age = 23
    ↓
set age(value)
    ↓
validate value
    ↓
this.#age = 23

So the getter/setter gives me controlled access to the private field.

30. Prototype Methods vs Instance Properties

This is another important distinction.

Consider:

class User {
    constructor(name) {
        this.name = name;
    }

    greet() {
        console.log(`Hello ${this.name}`);
    }
}

Each object has its own:

name

But greet() is shared through:

User.prototype

Conceptually:

user1 ─────┐
           │
user2 ─────┼──→ User.prototype → greet()
           │
user3 ─────┘

This is one reason prototypes are important.

The method does not need to be separately recreated on every instance.

31. My Final Mental Model

This is how I now connect the topics:

                    JavaScript Objects
                           │
                           ▼
                    Prototype System
                           │
              ┌────────────┴────────────┐
              ▼                         ▼
       Prototype Chain            Object.create()
              │
              ▼
           Classes
              │
        ┌─────┴─────┐
        ▼           ▼
     extends      static
        │
        ▼
      super

For controlling internal state:

                Encapsulation
                     │
             ┌───────┴────────┐
             ▼                ▼
         Closures          #private
             │                │
             └───────┬────────┘
                     ▼
              Controlled Data
                     │
              ┌──────┴──────┐
              ▼             ▼
           getter         setter

32. The Main Things I Need to Remember

Prototype

An object can look at another object for properties/methods.

Prototype chain

JavaScript searches from the object upward until it finds the property.

Object.create()

const child = Object.create(parent);

Creates an object whose prototype is parent.

Constructor function

function Bike(...) {}

Acts like a blueprint for creating objects with new.

Constructor + prototype

Bike.prototype.method = function () {};

Stores a shared method on the prototype.

Constructor inheritance

Child.prototype = Object.create(Parent.prototype);

Creates the parent-child prototype relationship.

class

class Bike {}

Provides cleaner syntax while still using prototypes underneath.

extends

class SuperBike extends Bike {}

Creates class inheritance.

super

super(...);

Accesses the parent constructor or parent method.

static

static info() {}

Belongs to the class itself, not its instances.

Getter

get age() {}

Runs when I read:

person.age

Setter

set age(value) {}

Runs when I write:

person.age = value;

_field

_age

Convention only. Not truly private.

#field

#age

Actual JavaScript private field.

Closure

A function can remember variables from its outer scope.

Conclusion

The biggest thing I learned is that these features are not random, separate JavaScript concepts.

The prototype system is the foundation.

Object
  ↓
Prototype
  ↓
Prototype Chain
  ↓
Inheritance
  ↓
Classes

And for controlling internal state:

Encapsulation
    ├── Closures
    └── Private fields (#field)

Getters and setters can then provide a controlled way to access that state:

getter → read
setter → change

The most important idea for me is:

JavaScript classes do not replace prototypes. Classes provide cleaner syntax for working with JavaScript's prototype-based object system.

And the privacy rule I want to remember is:

_field is a convention, while #field provides actual private class fields.

If I understand the prototype chain first, the rest of JavaScript's object-oriented features become much easier to understand.

DE
Source

This article was originally published by DEV Community and written by koushikmaya.

Read original article on DEV Community
Back to Discover

Reading List