DEV Community

koushikmaya
koushikmaya

Posted on

# 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();
Enter fullscreen mode Exit fullscreen mode

Output:

Barking
Eating
Enter fullscreen mode Exit fullscreen mode

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

JavaScript finds it through dog's prototype.

dog
 ↓
animal
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

I never created toString() inside person.

So how does it work?

JavaScript searches:

person
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

toString() is found on Object.prototype.

Property lookup in simple steps

When I write:

object.someProperty
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The search is:

dog
 ↓
animal          ← eat() found here
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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

Person.prototype
Enter fullscreen mode Exit fullscreen mode

The chain is:

person
 ↓
Person.prototype
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

__proto__

__proto__ gives access to an object's actual prototype.

console.log(person.__proto__ === Person.prototype);
Enter fullscreen mode Exit fullscreen mode

Output:

true
Enter fullscreen mode Exit fullscreen mode

For modern code, I should prefer:

Object.getPrototypeOf(person);
Enter fullscreen mode Exit fullscreen mode

instead of directly using __proto__.


4. Object.create()

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

Syntax:

Object.create(prototype);
Enter fullscreen mode Exit fullscreen mode

Example:

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

const dog = Object.create(animal);

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

dog.eat();
dog.bark();
Enter fullscreen mode Exit fullscreen mode

The relationship is:

dog
 ↓
animal
Enter fullscreen mode Exit fullscreen mode

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

My simple way of remembering it

const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Here:

obj2
 ↓
obj
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

I can think of this as a blueprint.

Then:

const bike1 = new Bike("Yamaha", "Blue", 1000);
Enter fullscreen mode Exit fullscreen mode

creates an object.

Conceptually:

bike1
├── company → "Yamaha"
├── colour  → "Blue"
└── cc      → 1000
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The important thing is that fun() is on:

Bike.prototype
Enter fullscreen mode Exit fullscreen mode

not copied directly into every object.

The chain is:

bike1
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

When I call:

bike1.fun();
Enter fullscreen mode Exit fullscreen mode

JavaScript looks for fun().

It checks:

bike1                 → not found
Bike.prototype        → found
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Actual constructor-function inheritance needs a second constructor.


9. Constructor Function + Prototype Inheritance

Now I can make:

Bike
 ↓
SuperBike
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Where does inheritance actually happen?

This line:

SuperBike.prototype = Object.create(Bike.prototype);
Enter fullscreen mode Exit fullscreen mode

is the important inheritance line.

It creates:

bike1
 ↓
SuperBike.prototype
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

So:

bike1.fun();
Enter fullscreen mode Exit fullscreen mode

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


10. Why Bike.call()?

This line:

Bike.call(this, company, colour, cc);
Enter fullscreen mode Exit fullscreen mode

handles the parent's properties.

The parent constructor has:

function Bike(company, colour, cc) {
    this.company = company;
    this.colour = colour;
    this.cc = cc;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

Then SuperBike adds:

this.topspeed = topspeed;
Enter fullscreen mode Exit fullscreen mode

So I remember the pattern like this:

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

Object.create(Bike.prototype)
      ↓
Get parent's prototype methods
Enter fullscreen mode Exit fullscreen mode

11. Object.create() vs Constructor Inheritance

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

Object.create()

const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now I have:

Child
 ↓
Parent
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

This looks very different from:

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

Bike.prototype.fun = function () {
    // ...
};
Enter fullscreen mode Exit fullscreen mode

But the important thing is:

Classes still use prototypes underneath.

The class method is placed on:

Bike.prototype
Enter fullscreen mode Exit fullscreen mode

rather than copied separately into every instance.

So:

class syntax
     ↓
prototype system underneath
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

The inheritance chain is approximately:

bike
 ↓
SuperBike.prototype
 ↓
Bike.prototype
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

This:

super(company, colour);
Enter fullscreen mode Exit fullscreen mode

calls the parent constructor.

It is similar in purpose to:

Bike.call(this, company, colour);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

A static method belongs to the class itself:

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

Bike.info();
Enter fullscreen mode Exit fullscreen mode

I should not do:

const bike = new Bike();

bike.info(); // ❌
Enter fullscreen mode Exit fullscreen mode

because info() is static.

Static property

class Bike {
    static wheels = 2;
}

console.log(Bike.wheels);
Enter fullscreen mode Exit fullscreen mode

The important difference is:

Normal method
     ↓
instance

Static method
     ↓
class itself
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Here info is attached directly to Bike.

I don't use:

const bike = new Bike("Yamaha");

bike.info(); // ❌
Enter fullscreen mode Exit fullscreen mode

The main idea is:

Bike.info()
    ↓
belongs to Bike itself

bike.info()
    ↓
looks for an instance method
Enter fullscreen mode Exit fullscreen mode

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());
Enter fullscreen mode Exit fullscreen mode

I have to use:

user.getFullName();
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Now I use:

user.fullName
Enter fullscreen mode Exit fullscreen mode

not:

user.fullName()
Enter fullscreen mode Exit fullscreen mode

The simple meaning is:

get → when I READ the property, run this code
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Output:

25
26
Enter fullscreen mode Exit fullscreen mode

What is happening?

When I write:

console.log(p1.age);
Enter fullscreen mode Exit fullscreen mode

I am reading age.

So the getter runs:

get age() {
    return this._age;
}
Enter fullscreen mode Exit fullscreen mode

It returns:

25
Enter fullscreen mode Exit fullscreen mode

Then when I write:

p1.age = 26;
Enter fullscreen mode Exit fullscreen mode

I am setting/changing age.

So the setter runs:

set age(newAge) {
Enter fullscreen mode Exit fullscreen mode

Here:

newAge = 26
Enter fullscreen mode Exit fullscreen mode

The condition:

if (newAge < 0)
Enter fullscreen mode Exit fullscreen mode

is false.

So:

this._age = newAge;
Enter fullscreen mode Exit fullscreen mode

changes the stored value to:

_age = 26
Enter fullscreen mode Exit fullscreen mode

When I read it again:

console.log(p1.age);
Enter fullscreen mode Exit fullscreen mode

the getter returns 26.


19. Why _age?

I use:

this._age
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

The idea is:

_age
 ↓
internal storage

age
 ↓
getter/setter interface
Enter fullscreen mode Exit fullscreen mode

This also prevents a common problem.

If I wrote:

set age(newAge) {
    this.age = newAge;
}
Enter fullscreen mode Exit fullscreen mode

the setter would call itself again and again.

So instead I store the actual value in:

this._age
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Now:

account.balance = 5000;
Enter fullscreen mode Exit fullscreen mode

is allowed.

But:

account.balance = -5000;
Enter fullscreen mode Exit fullscreen mode

is rejected.

So:

Getter
  ↓
Controls reading

Setter
  ↓
Controls changing
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Someone can do:

account.balance = 999999;
Enter fullscreen mode Exit fullscreen mode

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;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Create an account:

const account = BankAccount(1000);
Enter fullscreen mode Exit fullscreen mode

Use it:

account.deposit(500);

console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Enter fullscreen mode Exit fullscreen mode

But:

console.log(account.balance);
Enter fullscreen mode Exit fullscreen mode

returns:

undefined
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Use it:

const account = new BankAccount(1000);

account.deposit(500);

console.log(account.getBalance());
Enter fullscreen mode Exit fullscreen mode

Output:

1500
Enter fullscreen mode Exit fullscreen mode

But this is not allowed:

console.log(account.#balance);
Enter fullscreen mode Exit fullscreen mode

#balance is actually private to the class.


24. _field vs #field

This distinction is very important.

this._balance
Enter fullscreen mode Exit fullscreen mode

means:

"I intend this to be an internal property."

But JavaScript does not enforce that.

Someone can still do:

account._balance;
Enter fullscreen mode Exit fullscreen mode

Whereas:

this.#balance
Enter fullscreen mode Exit fullscreen mode

is a real private field.

So:

_balance
   ↓
Convention only

#balance
   ↓
Actual JavaScript private field
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

This method:

#validateAmount()
Enter fullscreen mode Exit fullscreen mode

can only be used inside the class.

Outside code cannot call:

account.#validateAmount(100);
Enter fullscreen mode Exit fullscreen mode

26. Closures vs Private Fields

Both can hide internal state.

Closure

function Counter() {
    let count = 0;

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

        getCount() {
            return count;
        }
    };
}
Enter fullscreen mode Exit fullscreen mode

Here count is hidden inside the function scope.

Private field

class Counter {
    #count = 0;

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

    getCount() {
        return this.#count;
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Output:

My name is Koushik and I am 22 years old.
Koushik is studying JavaScript
22
23
Human
Enter fullscreen mode Exit fullscreen mode

28. Understanding the Prototype Chain in This Example

When I write:

const student = new Student(
    "Koushik",
    22,
    "JavaScript"
);
Enter fullscreen mode Exit fullscreen mode

the object is connected to prototypes roughly like this:

student
 ↓
Student.prototype
 ↓
Person.prototype
 ↓
Object.prototype
 ↓
null
Enter fullscreen mode Exit fullscreen mode

If I call:

student.study();
Enter fullscreen mode Exit fullscreen mode

JavaScript finds study() on:

Student.prototype
Enter fullscreen mode Exit fullscreen mode

If I call:

student.introduce();
Enter fullscreen mode Exit fullscreen mode

JavaScript searches:

student
 ↓
Student.prototype
 ↓
Person.prototype       ← introduce() found
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

So:

console.log(student.age);
Enter fullscreen mode Exit fullscreen mode

flows like:

student.age
    ↓
get age()
    ↓
this.#age
    ↓
private value
Enter fullscreen mode Exit fullscreen mode

When I write:

student.age = 23;
Enter fullscreen mode Exit fullscreen mode

the setter runs:

student.age = 23
    ↓
set age(value)
    ↓
validate value
    ↓
this.#age = 23
Enter fullscreen mode Exit fullscreen mode

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}`);
    }
}
Enter fullscreen mode Exit fullscreen mode

Each object has its own:

name
Enter fullscreen mode Exit fullscreen mode

But greet() is shared through:

User.prototype
Enter fullscreen mode Exit fullscreen mode

Conceptually:

user1 ─────┐
           │
user2 ─────┼──→ User.prototype → greet()
           │
user3 ─────┘
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

For controlling internal state:

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

32. The Main Things I Need to Remember

Prototype

An object can look at another object for properties/methods.
Enter fullscreen mode Exit fullscreen mode

Prototype chain

JavaScript searches from the object upward until it finds the property.
Enter fullscreen mode Exit fullscreen mode

Object.create()

const child = Object.create(parent);
Enter fullscreen mode Exit fullscreen mode

Creates an object whose prototype is parent.

Constructor function

function Bike(...) {}
Enter fullscreen mode Exit fullscreen mode

Acts like a blueprint for creating objects with new.

Constructor + prototype

Bike.prototype.method = function () {};
Enter fullscreen mode Exit fullscreen mode

Stores a shared method on the prototype.

Constructor inheritance

Child.prototype = Object.create(Parent.prototype);
Enter fullscreen mode Exit fullscreen mode

Creates the parent-child prototype relationship.

class

class Bike {}
Enter fullscreen mode Exit fullscreen mode

Provides cleaner syntax while still using prototypes underneath.

extends

class SuperBike extends Bike {}
Enter fullscreen mode Exit fullscreen mode

Creates class inheritance.

super

super(...);
Enter fullscreen mode Exit fullscreen mode

Accesses the parent constructor or parent method.

static

static info() {}
Enter fullscreen mode Exit fullscreen mode

Belongs to the class itself, not its instances.

Getter

get age() {}
Enter fullscreen mode Exit fullscreen mode

Runs when I read:

person.age
Enter fullscreen mode Exit fullscreen mode

Setter

set age(value) {}
Enter fullscreen mode Exit fullscreen mode

Runs when I write:

person.age = value;
Enter fullscreen mode Exit fullscreen mode

_field

_age
Enter fullscreen mode Exit fullscreen mode

Convention only. Not truly private.

#field

#age
Enter fullscreen mode Exit fullscreen mode

Actual JavaScript private field.

Closure

A function can remember variables from its outer scope.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

And for controlling internal state:

Encapsulation
    ├── Closures
    └── Private fields (#field)
Enter fullscreen mode Exit fullscreen mode

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

getter → read
setter → change
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)