- INDEX
Paradigm: An organized approach with clear structure.
-
OOP Paradigm replaces the "procedural paradigm" by encapsulating data and methods into objects.
- Benefits:
- More readable and maintainable code
- Easier to add features
- Efficient in memory usage (be)
- Benefits:
-
Objects are self-contained units of code and data.
- They're the building blocks of OOP applications.
- They interact through public interfaces.
-
Class: A blueprint for objects. It describes the properties and methods that objects created from it will have.

- The name of the class can then be used to generate instances of the class (pattern of objects).
- It's a common convention to capitalize the first letter of a class name (UpperCamelCase).
To design an OOP application using classes, follow these 4 principles:
-
Abstraction
-
Encapsulation
-
Inheritance
-
Polymorphism
JavaScript's OOP approach differs from other languages as it uses prototypal inheritance rather than classical inheritance.
It's a container that organizes things into units that model real-world objects. It's a collection of properties and methods.
-
objects can store properties. object property is actually a more flexible and powerful thing than just a
key-valuepairs.-
Object properties, besides a value, have three special attributes (so-called
“flags”):-
writable– iftrue, the value can be changed, otherwise it’s read-only. -
enumerable– iftrue, then listed in loops, otherwise not listed.- we can set
enumerable:false. Then it won’t appear in a for..in loop,
let user = { name: 'John', toString() { return this.name; } }; Object.defineProperty(user, 'toString', { enumerable: false }); // Now our toString disappears: for (let key in user) alert(key); // name
- we can set
-
configurable– iftrue, the property can be deleted and these attributes can be modified, otherwise not.Please note:
configurable: falseprevents changes of property flags and its deletion, while allowing to change its value. -
The method
Object.getOwnPropertyDescriptorallows to query the full information about a property.let descriptor = Object.getOwnPropertyDescriptor(obj, propertyName);
-
-
Accessor properties are represented by
“getter”and“setter”methods. In an object literal they are denoted by get and set:let obj = { get propName() { // getter, the code executed on getting obj.propName }, set propName(value) { // setter, the code executed on setting obj.propName = value } }; // ------------------------------------------------- let user = { name: 'John', surname: 'Smith', get fullName() { return `${this.name} ${this.surname}`; }, set fullName(value) { [this.name, this.surname] = value.split(' '); } }; // set fullName is executed with the given value. user.fullName = 'Alice Cooper'; alert(user.fullName); // Alice Cooper
-
-
Note:
- What will happen if you use the
fullNamesetter to add a new object property also calledfullName?- It will cause an infinite loop, because
this.fullName = valuewill call the setter again and again. - To fix this issue, we can use another variable name, e.g.
this._fullName = value.
- It will cause an infinite loop, because
- What will happen if you use the
Getters/setters can be used as wrappers over “real” property values to gain more control over operations with them.
- Why using getters/setters ?
- Perform operations on the attribute before setting the attribute or returning it
- check the value before setting the attribute or returning it
- One of the great uses of accessors is that they allow to take control over a “regular” data property at any moment by replacing it with a
getterand asetterand tweak its behavior.
let user = {
get name() {
return this._name;
},
set name(value) {
if (value.length < 4) {
alert('Name is too short, need at least 4 characters');
return;
}
this._name = value; // The name is stored in _name property, and the access is done via getter and setter.
}
};
user.name = 'Pete';
alert(user.name); // Pete
user.name = ''; // Name is too short...Each object has a prototype, which contains methods and properties that are shared among all instances of that object.

-
Objectsare linked to aprototypeobject -> "Prototypal Inheritance / Delegation"- The top of the chain is
null, which means that the chain ends there. - The one before the top is
Object.prototype, which is the default prototype for all objects. because all things in javascript are objects. .prototypeis a property of aconstructor functionthat is used to set theprototypeof the objects created by that constructor function. (It's not the actual prototype of the object, and It's not available for objects or any other non-constructor functions)- Instead, the actual prototype of an object is stored in a hidden property called
[[Prototype]](__proto__in modern browsers)
- Instead, the actual prototype of an object is stored in a hidden property called
- The top of the chain is
-
Ways to implement
prototypal inheritancein Javascript: -
In JavaScript, each
constructor-function/classhas a hidden[[Prototype]]property shared by all instances, referencing another object ornull.-
[[Prototype]]is internal and hidden but can be set using__proto__:rabbit.__proto__ = animal; // sets rabbit.[[Prototype]] = animal
- It's advised to use
Object.getPrototypeOf/Object.setPrototypeOfinstead of__proto__. -> it can benullor anobject. - It's added by browsers for easier prototype access.
- It's advised to use
-
-
"this"is not affected by prototypes.- In a method call,
thisis always the object before the dot, regardless of where the method is found. - Inherited methods modify only the inheriting object's state.
- In a method call,
-
To add something to the
prototypeof something, we can use theprototypeproperty of theconstructor function.function Rabbit() {} // Replace the default Rabbit.prototype Rabbit.prototype = { jumps: true }; // --------------- or --------------- // // Add to the existing one Rabbit.prototype.jumps = true; // now all rabbits can jump let rabbit = new Rabbit(); alert(rabbit.jumps); // true
-
Trick: prototype chaining can also be done like this:
let animal = { eats: true }; function Rabbit(name) { this.name = name; __proto__: animal; // works, but not recommended as it's not standard }
why we do this instead of declaring the function with the class properties each time we instantiate a new class (object) ?
- because we don't want to create the same function that have same functionality each time for each object -> (Bad Performance)
- instead we move all these methods (functions) to a parent object so that it won't be created each time (Bundle all common functions together + Make a Bond to that object by a hidden property ->
__proto__)__proto__hidden property is added in the background using the "new" keyword that points to the prototype object-property in the parent class
-
Every function has the "
prototype" property even if we don’t supply it. -
The default "
prototype" is an object with the only propertyconstructorthat points back to the function itself.function Rabbit() {} /* default prototype Rabbit.prototype = { constructor: Rabbit }; */ // Adding properties to the prototype (functions) function jump() { alert(`${this.name} jumps!`); } Rabbit.prototype.jump = jump; // now all rabbits can jump
-
Javascript uses the
__proto__reference to giveobjects,functionsandarraysa bunch of bonus functionality. as all things in javascript are objects, and all objects by default have__proto__property that points to theprototypeof theconstructor functionthat created them.

let arr = [1, 2, 3]; arr.slice(1); // [2, 3] -> Where is this method coming from? // it inherits from Array.prototype? arr.toString(); // "1,2,3" -> Where is this method coming from? // it inherits from Array.prototype, doesn't find it there, then checks Object.prototype, where it's found.
-
Other built-in prototypes:
let arr = [1, 2, 3]; // it inherits from Array.prototype? alert(arr.__proto__ === Array.prototype); // true // then from Object.prototype? alert(arr.__proto__.__proto__ === Object.prototype); // true // and null on the top. alert(arr.__proto__.__proto__.__proto__); // null // notes alert(Array.prototype.__proto__ === Object.prototype); // true alert(Array.prototype.isPrototypeOf(Array)); // false alert(Array.hasOwnProperty('map')); // false alert(Array.prototype.hasOwnProperty('map')); // true
- In modern programming, there is only one case where modifying native prototypes is approved. That’s polyfilling.
- Polyfilling is a term for making a substitute for a method that exists in the JavaScript specification, but is not yet supported by a particular JavaScript engine.
-
How to add a new method to native prototypes?
-
for example: adding
getLastYear()method to theDateobjectDate.prototype.getLastYear = function () { return this.getFullYear() - 1; }; // Note: don't use arrow functions here, because they don't have their own `this` keyword, and we want to use the `this` keyword to refer to the `Date` object. new Date('1900-10-10').getLastYear(); // 1899
-
example: modify
mapmethod of theArrayobject (not recommended)Array.prototype.map = function () { let arr = []; for (let i = 0; i < this.length; i++) { arr.push(this[i] + '🗺'); } return arr; }; console.log([1, 2, 3].map()); // [ "1🗺", "2🗺", "3🗺" ]
-
It's a function that creates an object type, and prepares the object for use. which implements a reusable object creation code.
-
It must be called with the
newkeyword. as it's used to create a new instance of an object.function User(name) { this.name = name; this.isAdmin = false; } let user = new User('Jack');
-
Their name should start with a capital letter to indicate that they are constructors and not regular functions
-
Usually, constructors do not have a
returnstatement. Their task is to write all necessary stuff intothis, and it automatically becomes the result. But if there is areturnstatement, then:- If
returnis called with an object, then the object is returned instead ofthis. - If
returnis called with a primitive, it’s ignored.
function BigUser() { this.name = 'John'; return { name: 'Godzilla' }; // <-- returns this object } alert(new BigUser().name); // Godzilla, got that object
- If
-
Note that when defining a function in javascript, it's both a function and an object at the same time. (function + object) combo
- So, we can add properties to the function itself.
- This is the most important part of the
constructor functionbecause we can add methods to theprototypeof theconstructor functionand then all objects created from thatconstructor functionwill inherit those methods.
-
This is how build-in objects like
Array,MaporSetare actually implemented -
This only works with functions created with the
functionkeyword, not with arrow functions (because arrow functions don't have their ownthiskeyword) -
Note: As mention before, only constructor functions have access to the
prototypeproperty. Allowing us to add methods to theprototypeof the constructor function.function User(name) { this.name = name; } User.prototype.sayHi = function () { alert(this.name); }; let user = new User('John'); user.sayHi(); // John
It's a keyword that automates the hard work (process of creating an object and linking it to the prototype of the constructor function).
-
When a function is executed with
newkeyword, it does the following steps:- 1️⃣ Creating a new empty object and assigning it to
thiskeyword which is a reference to the new object (asthisis returned by default). - 2️⃣ creating a new hidden property
__proto__and setting it to theprototypeproperty of theconstructor function. - 3️⃣ The function body executes. Usually it modifies
this, adds new properties to it. - 4️⃣ The value of
thisis returned. (The new object is returned)
- 1️⃣ Creating a new empty object and assigning it to
-
All is that is possible because (functions in JS are both function & objects combo)

- That's why we can use the "dot notation" to add properties to the function (constructor function) and also to the object created from it.
-
So, the
newkeyword behind the scenes does all the prototype stuff for us.// OLD function userCreator(name) { const newUser = object.create(userFunctionStore); newUser.name = name; return newUser; } const userFunctionStore = { sayHello: function () { console.log('Hello!'); } }; const user1 = userCreator('Phil'); user1.sayHello(); // --------------------------------------------------------------- // NEW function User(name) { this.name = name; } User.prototype.sayHello = function () { console.log('Hello!' + this.name); }; const user1 = new User('Phil'); user1.sayHello(); // Hello! Phil // --------- Steps behind the scenes --------- // // the `new` keyword does the following: // 1. create a new function + an empty object with the hidden property (prototype) // 2. assign the new object to the (`this`) keyword // 3. sets the `__proto__` of the new object to the prototype of the function // 4. return the new object
-
- So, when the
increment()method is called, these steps will happen:- we will check if
increment()method exists on theuser1object, if not then we will look up the__proto__chain to find the method. which leads us to theprototypeof theUserconstructor function. - Then if found, the method will be executed with a new "execution context" and the
thiskeyword will point to theuser1object. - Now inside the
increment()method,thiskeyword will point to theuser1object. So, thethis.score = user1.scorewill be incremented by1.
- we will check if
- That is why we may find problems with
thiskeyword in theprototypemethods if we create the method using thearrow functionbecause thethiskeyword will point to thewindowobject instead of the object that we want to modify.
- So, when the
-
We use
newkeyword also when we want to create a new instance of a class, because classes are just syntactic sugar forconstructor functionsclass User { constructor(name) { this.name = name; } } let user = new User('John');
-
NOTES:
-
other built in objects like arrays have the hidden
__proto__property -
Here are the things that you should/shouldn't use the
newkeyword with:- Use it with
constructor functions(classes) that you want to create an instance of an object from (type of object)Object(),Array(),Date(),Function(),Promise(),RegExp(),Error(),Map(),Set()
- Don't use it with things that you don't want to become an object-type instead of primitive-type
Number(),String(),Boolean()
- Use it with
-
The major difference from other ways we’ve seen is that the function is created literally from a string; For example, we can receive a new function from a server and then execute it:
- It is used in very specific cases, like when we receive code from a server, or to dynamically compile a function from a template, in complex web-applications.
-
-
Enhanced object literals :
- It's a way to simplify the creation of objects by allowing us to use shorthand property names and shorthand method names.
const name = 'Jonas'; const age = 46; const jonas = { name, // instead of name: name ✅ age // instead of age: age ✅ // also the ability to write methods like this greet() { console.log(`Hey ${this.name}`); } // instead of greet: function() {...} };
-
⚠️ You should never create a method inside of a constructor function.- Creating methods inside constructors leads to multiple copies of the method for each instance, which is bad for performance.
-
Use prototypal inheritance instead:
function Person(firstName, birthYear) { // Instance properties this.firstName = firstName; this.birthYear = birthYear; // Never do this ❌ this.calcAge = function () { console.log(2037 - this.birthYear); }; } // Instead use: prototypal inheritance ✅ Person.prototype.calcAge = function () { console.log(2037 - this.birthYear); }; const jonas = new Person('Jonas', 1991); jonas.calcAge(); // 46
-
Why? because if we create a method inside of a constructor function, then that method will be attached to each object that we create using that constructor function. So, if we create a hundred objects using that constructor function, then we would essentially create a hundred copies of that method in memory. This is bad for performance.
-
Use prototypal inheritance to add methods to the constructor's prototype.
- This ensures only one copy of the method is shared among all instances. (all instances of the object will share the same method)
- This is automatically done for ES6 classes methods.
It's another way to implement prototypal inheritance in JavaScript. It's a bit more modern than using new keyword with constructor functions.
- It's the easiest and most straightforward way to link an object to a prototype object.
- It works by passing in an object that will be the
prototypeof the new object that we create. - So, It enables us to create
prototypesfrom other objects.
const PersonProto = {
init(name, birthYear) {
this.name = name;
this.birthYear = birthYear;
}
calcAge() {
console.log(2037 - this.birthYear);
},
};
// ------------- using object.create ------------- //
// method-1
// make (PersonProto) to be the prototype of (steven)
const steven = Object.create(PersonProto);
console.log(steven.__proto__ === PersonProto); // true
steven.name = 'Steven';
steven.birthYear = 2002;
steven.calcAge();
// method-2
const sarah = Object.create(PersonProto);
sarah.init('Sarah', 1979);
sarah.calcAge();-
Difference between
object.create()andnewkeyword with constructor functions:object.create()builds an object that inherits directly from the one that we passed into the first argument.newkeyword with constructor functions, its created object inherits from the constructor'sprototypeproperty.
Object.create()is the least used way to create objects in JS.
In object-oriented programming, a
classis an extensible program-code-template for creating objects, providing initial values for state (member variables) and implementations of behavior (member functions or methods).
It's a modern alternative to constructor functions syntax. which is more readable and easier to understand. (It doesn't behave like classes in other languages)
-
The problem with the previous way of creating objects using constructor functions is that we're writing our shared methods separately from the constructor function, and then we're manually adding them to the prototype of the constructor function. Other languages let us do this all in one place, and that's what
classesin JavaScript are all about.
- Both are the same under the hood, but the
classsyntax is just a more modern and cleaner way to work withprototypal inheritancein JavaScript.
- Both are the same under the hood, but the
-
ES6 classes are just a new way to write
constructor functionsandprototypal inheritancein JavaScript. (Syntactic sugar)-
class automatically creates a (function + object) combo like
constructor functiondoes- the object created will have the methods added to the automatically created
prototypeobject of the class, instead of manually adding the methods to theprototypeobject
// Class Expression // (unnamed) let User = class { sayHi() { alert('Hello'); } }; new User().sayHi(); // Hello // (named) let User = class MyClass { sayHi() { alert(MyClass); // MyClass name is visible only inside the class } }; new User().sayHi(); // works, shows MyClass definition alert(MyClass); // error, MyClass name isn't visible outside of the class // --------------------------------------------------------------- // // Class declaration class User { sayHi() { alert('Hello'); } }
- the object created will have the methods added to the automatically created
-
-
in the code below, What
class User {...}construct really does is:class User { constructor(name) { this.name = name; } sayHi() { alert(this.name); } }
- Creates a function named
PersonCl, that becomes the result of the class declaration. The function code is taken from the constructor method (assumed empty if we don’t write such method). - Stores class methods, such as sayHi, in
User.prototype.
- Creates a function named
It's a special method for creating and initializing an object created with a class. It's called when we create a new instance of the class.
-
It's a method that is called when we create a new object from a class.
-
It's usually used to:
-
set up (initialize) the object's properties.
class User { constructor(name) { this.name = name; } } let user = new User('John'); alert(user.name); // John
-
Validate the input data
class User { constructor(name) { if (!name) { throw new Error('Name is required.'); } if (typeof name !== 'string') { throw new Error('Name must be a string.'); } this.name = name; } } let user = new User(); // Error: Name is required.
-
-
Constructor functions return
thisby default, but in classes, we don't need to returnthisbecause it's done automatically. So They returnundefinedby default.class User { constructor(name) { this.name = name; } } let user = new User('John'); alert(user); // [object Object] alert(user.constructor()); // undefined ⚠️
They're special methods that are used to define the way to retrieve or change the values of an object's properties.
They're used to add an extra layer of security to our data, and to do some extra things behind the scenes when we try to get or set a property.
-
getter:-
show a method as a property when we access it.
-
get value out of an object by writing a property instead of writing a method
-
Note that when using it the value of it will be reactive (will always have the latest values of the properties)
class PersonCl { constructor(fullName, birthYear) { this.fullName = fullName; this.birthYear = birthYear; } // ... get age() { return 2037 - this.birthYear; } } const jessica = new PersonCl('Jessica Davis', 1996); console.log(jessica.age); // 41
-
It can also be used to access private properties (without being able to change them)
class PersonCl { constructor(fullName, birthYear) { this._fullName = fullName; this._birthYear = birthYear; } // ... get age() { return 2037 - this._birthYear; } } const jessica = new PersonCl('Jessica Davis', 1996); console.log(jessica.age); // 41 jessica.age = 23; // 41 (not changed)
-
-
setter:-
Allows us to change the value of a property in an object (with some conditions/validations if we want)
-
Usually used to set the value of private properties to make sure that the value is valid.
class PersonCl { constructor(fullName, birthYear) { this.fullName = fullName; this.birthYear = birthYear; } // ... set fullName(name) { if (name.includes(' ')) this._fullName = name; else alert(`${name} is not a full name!`); } } const jessica = new PersonCl('Jessica Davis', 1996); jessica.fullName = 'Jess Davis'; console.log(jessica); // PersonCl {fullName: "Jess Davis", birthYear: 1996}
-
if you have a setter for a property that is already defined in the constructor, then you need to create basically a new property with the
underscore ( _ )in front of it.
-
-
Any class property can have a
getterand asetter.class User { constructor(firstName, lastName) { this.firstName = firstName; this.lastName = lastName; } get fullName() { return `${this.firstName} ${this.lastName}`; } set fullName(value) { const parts = value.split(' '); this.firstName = parts[0]; this.lastName = parts[1]; } } let user = new User('John', 'Doe'); alert(user.fullName); // John Doe this.fullName = 'Alice Cooper'; alert(user.firstName); // Alice
They're methods that are called on the class itself, not on the instances of the class.
-
They're not available for instances
- Usually, static methods are used to implement functions that belong to the class as a whole, but not to any particular object of it.
-
They can be called without the need to create an instance of the class (because they are not available for instances and they're factory methods)
-
it can't access the instance properties, only the class properties.
class User { constructor(name) { this.name = name; } static sayHi() { alert(this.name); // undefined (because it's a static method and it can't access the instance properties) } } User.sayHi(); // undefined
-
Use Cases:
-
It can be used to group utility functions together in a class without the need to create an instance of the class:
class MyMath { // similar to Math object static sum(a, b) { return a + b; } static multiply(a, b) { return a * b; } // ... } alert(MyMath.sum(1, 2)); // 3
-
It can be used as a "factory method" (a factory is an object for creating other objects (instantiate instance of the class); formally, it is a function or method that returns objects of a varying prototype or class from some method call, which is assumed to be "new" ):
class Article { constructor(title, date) { this.title = title; this.date = date; } static createTodays() { // remember, this = Article return new this("Today's digest", new Date()); } } let article = Article.createTodays(); // without the need to create an instance of the class alert(article.title); // Today's digest
-
They're properties that are attached to the class itself, not to the instances of the class.
-
They look like regular class properties, but prepended by static
class Article { static publisher = 'Ilya Kantor'; } alert(Article.publisher); // Ilya Kantor
-
Static properties are inherited using prototypes.
- But built-in classes are an exception. They don’t inherit statics from each other.
- For example, both
ArrayandDateinherit fromObject, so their instances have methods fromObject.prototype. ButArray.[[Prototype]]does not reference Object, so there’s no, for instance,Array.keys()(orDate.keys()) static method.
-
Note that they usually, static properties are used to store class-wide data. instead of creating this field/constant in all instances.
They're used to initialize static fields in a class.
-
It's a block of code that is executed when the class is loaded into memory.
-
It's used to initialize static fields in a class by running only once when the class is loaded into memory (NOT the instance).
class User { static count = 0; static { alert('Hello, the class is loaded'); this.count = 1; } } alert(User.count); // 1
-
What examples do you think of that could be useful for static initialization blocks?
-
For example, you could use it to connect to a database, or to load some configuration data from a file, or to initialize some static fields in a class.
-
Also if you want initialize some static fields in a class differently based on the environment (development, production, testing, etc.)
class User { static count = 0; static { if (process.env.NODE_ENV === 'development') { this.count = 1; } else { this.count = 2; } } }
-
It's a methodology used for hiding information. It is based on the concept that object properties should not be exposed publicly to the outside world. Implementing Encapsulation in JavaScript prevents access to the variables by adding public entities inside an object, which the callers can use to achieve specific results.
-
In JavaScript, there are two types of object fields (properties and methods):
Public: accessible from anywhere. They comprise the external interface. Until now we were only using public properties and methods.Private: accessible only from inside the class (and subclasses). They are for internal operations.static: accessible from the class itself, not from instances or subclasses.- Example: a static method to create an instance of the class (factory method). or a static method that can be used to calculate math operations, and the class is just a container for these methods.
-
we use it to prevent the user from seeing specific properties or changing them
-
so we make these properties accessible from methods and not directly
-
They can be used to clean up the constructor and make it more readable
-
Protectedproperties are usually prefixed with an underscore_and Here we used getter/setter syntax (It's just a convention unlike#for private fields)class CoffeeMachine { _waterAmount = 0; // protected property setWaterAmount(value) { if (value < 0) value = 0; this._waterAmount = value; } getWaterAmount() { return this._waterAmount; } } const coffeeMachine = new CoffeeMachine(); coffeeMachine.setWaterAmount(100); alert(coffeeMachine.getWaterAmount()); // 100
-
the new
Privatefields syntax#: is a recent addition to the language. Not supported in JavaScript engines, or supported partially yet, requires polyfilling.- Privates should start with
#. They are only accessible from inside the class. - Unlike protected ones (with
_), private fields are enforced by the language itself. That’s a good thing.
class CoffeeMachine { #waterLimit = 200; // private property #fixWaterAmount(value) { if (value < 0) return 0; if (value > this.#waterLimit) return this.#waterLimit; } setWaterAmount(value) { this.#waterLimit = this.#fixWaterAmount(value); } } let coffeeMachine = new CoffeeMachine(); // can't access privates from outside of the class coffeeMachine.#fixWaterAmount(123); // Error ❌ coffeeMachine.#waterLimit = 1000; // Error ❌
-
Privatefields do not conflict withpublicones. We can have both private#waterAmountand publicwaterAmountfields at the same time. -
You can't use the private field before it's declared in the top of the class.
// Won't work ❌ class CoffeeMachine { constructor() { this.#waterAmount = 0; // Error: can't access before initialization } } // Works ✅ class CoffeeMachine { #waterAmount; constructor() { this.#waterAmount = 0; } }
- Privates should start with
-
Example of all types of properties and methods:
// Encapsulation: Protected Properties and Methods // 1) Public fields // 2) Private fields // 3) Public methods // 4) Private methods // (there is also the static version) class Account { // 1) Public fields locale = navigator.language; // 2) Private fields #movements = []; // or _movements #pin; // we define it like this without a value as its value will be set in the constructor constructor(owner, currency, pin) { this.owner = owner; this.currency = currency; this.#pin = pin; // another way of Protected property (using (_) before property's name) // this._movements = []; // another way of Public property // this.locale = navigator.language; } // 3) Public methods // Public interface that will call the private method -> API provides access to the private property getMovements() { return this.#movements; } deposit(val) { this.#movements.push(val); // that's how we modify a private property (through a public method) return this; } withdraw(val) { this.deposit(-val); return this; } // 4) Private methods // #approveLoan(val) { _approveLoan(val) { return true; } }
-
Notes:
- In TypeScript, we have class access modifiers like
public,private, andprotectedthat are used to control the access to the class members. But in JavaScript, we don't have these access modifiers.
- In TypeScript, we have class access modifiers like
- Method chaining is the mechanism of calling a method on another method of the same object. This ensures a cleaner and readable code.
- Method chaining uses
thiskeyword in the object's class to access its methods. When a method returnsthis, it simply returns an instance of the object in which it is returned. in another word, to chain methods together :
class Account {
deposit(val) {
this.movements.push(val);
return this;
}
withdraw(val) {
this.deposit(-val);
return this;
}
}
const account1 = new Account('Jonas', 'EUR', 1111, []);
account1.deposit(250).withdraw(140);-
There're no classes in Javascript, this is just syntactic sugar
-
Sometimes people say that class is a “syntactic sugar” (syntax that is designed to make things easier to read, but doesn’t introduce anything new), because we could actually declare the same thing without using the class keyword at all
-
actually, It's "Not just a syntactic sugar"
-
a function created by
classis labelled by a special internal property[[IsClassConstructor]]: true. So it’s not entirely the same as creating it manually.-
The language checks for that property in a variety of places. For example, unlike a regular function, it must be called with
newkeyword:class User { constructor() {} } alert(typeof User); // function User(); // Error: Class constructor User cannot be invoked without 'new'
-
-
-
Class methods are non-enumerable. A class definition sets
enumerableflag tofalsefor all methods in the "prototype".- That’s good, because if we
for..inover an object, we usually don’t want its class methods.
- That’s good, because if we
-
Classes are always executed in strict mode. All code inside the class construct is automatically in strict mode.
-
-
In JavaScript, a
classis a kind of (hastypeequal to) function. -
There're no commas between class methods
-
Classes are NOT hoisted
- Even if we declare a class using a
classdeclaration, aclassis NOT hoisted, unlikefunctions
- Even if we declare a class using a
-
Classes are first-class citizens
- means that we can pass them into functions and also return them from functions
-
Classes are executed in
strict mode -
You can use Computed methods names
[…]class User { ['say' + 'Hi']() { alert('Hello'); } } new User().sayHi();
-
You can check if an object is an instance of a class using the
instanceofoperator.class User { constructor(name) { this.name = name; } } let user = new User('John'); alert(user instanceof User); // true
It's a way to create a new class that inherits from an existing class.
Here, inherence works by using the parent's factory function inside the child's factory function. and then linking the prototypes manually.
-
normal example without optimization
function User(name, score) { return { name: name, score: score, sayName: function () { console.log("I'm " + this.name); }, increment: function () { this.score++; } }; } const user1 = User('Phil', 5); user1.sayName(); // I'm Phil
- The problem here, is when we create a new object from the
Userfunction, we're creating a new copy of thesayNameandincrementmethods for each object that we create. which is bad for performance. - Instead, we should have the methods defined once and shared among all objects using the
prototype.
- The problem here, is when we create a new object from the
-
Complex example with explanation (with optimization)
-
parent factory function
// declaring the functions outside of the factory function so that they are not created every time we create a new object const userFunction = { sayName: function () { console.log("I'm " + this.name); }, increment: function () { this.score++; } }; // Parent const UserCreator = function (name, score) { const newUser = Object.create(userFunction); // creating a new object that inherits from the userFunction object (prototype) 🚀 newUser.name = name; newUser.score = score; return newUser; }; const user1 = UserCreator('Phil', 5); user1.sayName(); // My name is Phil
-
child factory function
// Child const PaidUserCreator = function (paidName, paidScore, accountBalance) { // 1. use the parent factory function const newPaidUser = UserCreator(paidName, paidScore); // 2. set the prototype of the new object to the parent object Object.setPrototypeOf(newPaidUser, paidUserFunction); // 3. add new properties to the new object newPaidUser.accountBalance = accountBalance; return newPaidUser; }; paidUserFunction = { increaseBalance: function () { this.accountBalance++; } }; Object.setPrototypeOf(paidUserFunction, userFunction); // Linking prototypes (manually) const paidUser1 = paidUserCreator('Alyssa', 8, 25); paidUser1.increaseBalance(); // 26 (from paidUserFunction) paidUser1.sayName(); // I'm Alyssa (from userFunctionStore)
-
Steps:
- use the parent factory function
- set the prototype of the new object to the parent object before adding new properties to the new object or returning it.
- add new properties to the new object
- return the new object
- Linking prototypes (manually) by using
Object.setPrototypeOf()for the child object's prototype to the parent object's prototype. So that now the child object will inherit the methods from the parent object if it doesn't find the method on itself. - Now, the child object will inherit the methods from the parent object.
-
These steps and images show how sub-classing works in JavaScript under the hood (low-level). which might be asked in a technical interview. "Tell me how sub-classing works in JavaScript under the hood?"
-
-
Note: This way of sub-classing is not used anymore, instead we use
ES6 classesto do the same thing.
Here, inheritance works by using the call or apply methods to call the Parent constructor function with the this keyword set to the Child object.
-
Simple example
// Parent const Person = function (firstName, birthYear) { this.firstName = firstName; this.birthYear = birthYear; }; Person.prototype.calcAge = function () { console.log(2037 - this.birthYear); }; // Child const Student = function (firstName, birthYear, course) { Person.call(this, firstName, birthYear); // calling the parent function with setting the "this" keyword to the child object this.course = course; }; // Linking prototypes Student.prototype = Object.create(Person.prototype); // Adding new method to the child object Student.prototype.introduce = function () { console.log(`My name is ${this.firstName} and I study ${this.course}`); }; // Creating an object from the child object const mike = new Student('Mike', 2020, 'Computer Science'); mike.introduce(); // My name is Mike and I study Computer Science
-
We use
newkeyword to create a new object from theParentconstructor function and then we usecallmethod to call theParentconstructor function with thethiskeyword set to theChildobject. -
Parent constructor function
const userCreator = function (name, score) { this.name = name; this.score = score; }; userCreator.prototype.sayName = function () { console.log("I'm " + this.name); }; userCreator.prototype.increment = function () { this.score++; }; const user1 = new userCreator('Phil', 5); user1.sayName(); // I'm Phil
-
Child constructor function
const paidUserCreator = function (paidName, paidScore, accountBalance) { userCreator.call(this, paidName, paidScore); this.accountBalance = accountBalance; }; paidUserCreator.prototype = Object.create(userCreator.prototype); paidUserCreator.prototype.increaseBalance = function () { this.accountBalance++; }; const paidUser1 = new paidUserCreator('Alyssa', 8, 25); paidUser1.increaseBalance(); // 26 paidUser1.sayName(); // I'm Alyssa
-
Notes:
-
Here, inheritance works by using the
callmethod to call thePersonconstructor function with thethiskeyword set to theStudentobject.Remember that
callandapplyallow us to manually control what thethiskeyword is in any function.const Student = function (firstName, birthYear, course) { Person.call(this, firstName, birthYear); // here we are not using the (new) word and we are using (Person) as a normal function and not a class, we just want to call the function and set the (this) keyword to the (Student) object this.course = course; }; // Linking prototypes Student.prototype = Object.create(Person.prototype); // here we use (Object.create) because we want to set the prototype "MANUALLY" Student.prototype.introduce = function () { console.log(`My name is ${this.firstName} and I study ${this.course}`); };
const PersonProto = { calcAge() { console.log(2037 - this.birthYear); } init(firstName, birthYear) { this.firstName = firstName; this.birthYear = birthYear; }, } // 1. set the __proto__ of the subClass from the __proto__ of the superClass const StudentProto = Object.create(PersonProto); // 2. add init properties to the subClass StudentProto.init = function (firstName, birthYearcourse) { PersonProto.init.call(this, firstName, birthYear); this.course = course; }; // 3. create an instance of the subClass using the baseClass const steven = Object.create(StudentProto); steven.init('Jay', 2010, 'Computer Science');
-
Here, inheritance works by using the extends keyword to set the __proto__ property of the Student class to the Person class.
Behind the scenes, it does the same thing as the previous way of creating objects using constructor functions, but it's just a more modern and cleaner way to work with prototypal inheritance in JavaScript -> It's just an abstraction over the constructor function way
-
Parent (super) class
// Parent class UserCreator { constructor(name, score) { this.name = name; this.score = score; } sayName() { console.log("I'm " + this.name); } increment() { this.score++; } } const user1 = new UserCreator('Phil', 4);
-
Child (sub) class
class PaidUserCreator extends UserCreator { constructor(paidName, paidScore, accountBalance) { super(paidName, paidScore); // call the parent constructor function (must be called first ⚠️) this.accountBalance = accountBalance; } increaseBalance() { this.accountBalance++; } } const paidUser1 = new PaidUserCreator('Alyssa', 8, 25); paidUser1.increaseBalance(); // 26 paidUser1.sayName(); // I'm Alyssa
- Good luck understanding the image 😅
-
Notes:
-
We do something in our method, but call the parent method before/after it or in the process. Classes provide the "
super" keyword for that.-
super.method(...)to call a parent method. -
super(...)to call a parent constructor (inside our constructor only). -
super()-> creates a hidden property__proto__to the object but here__proto__won't point toprototypeof the parent object, instead it will point to the object creator constructor function of the parent class- then the result (return value) from the
object creator constructor functionwill be put in the createdthisproperty
- then the result (return value) from the
-
-
Note that we use the
newkeyword to create a new object from theParentconstructor function and then we useextendskeyword to set the__proto__property of theChildclass to theParentclass.- The
extendskeyword is used to create a class as a child of another class. prototypesare automatically inherited with theextendsword.
- The
-
Here, inheritance works by using the Object.create() method to manually set the __proto__ property of the Child object to the Parent object.
// Parent
const PersonProto = {
calcAge() {
console.log(2037 - this.birthYear);
},
init(firstName, birthYear) {
this.firstName = firstName;
this.birthYear = birthYear;
}
};
// Child
const StudentProto = Object.create(PersonProto);
StudentProto.init = function (firstName, birthYear, course) {
PersonProto.init.call(this, firstName, birthYear);
this.course = course;
};
const jay = Object.create(StudentProto);In JavaScript we can only inherit from a single object. There can be only one [[Prototype]] for an object. And a class may extend only one other class. But sometimes that feels limiting. For instance, we have a class StreetSweeper and a class Bicycle, and want to make their mix: a StreetSweepingBicycle. There’s a concept that can help here, called “mixins”.
a mixin is a class containing methods that can be used by other classes without a need to inherit from it. In other words, a mixin provides methods that implement a certain behavior, but we do not use it alone, we use it to add the behavior to other classes.
// mixin
let sayHiMixin = {
sayHi() {
alert(`Hello ${this.name}`);
},
sayBye() {
alert(`Bye ${this.name}`);
}
};
// usage:
class User {
constructor(name) {
this.name = name;
}
}
// copy the methods
Object.assign(User.prototype, sayHiMixin);
// now User can say hi
new User('Dude').sayHi(); // Hello Dude!-
Mixins can make use of inheritance inside themselves:
let sayMixin = { say(phrase) { alert(phrase); } }; let sayHiMixin = { __proto__: sayMixin, // (or we could use Object.setPrototypeOf to set the prototype here) sayHi() { // call parent method super.say(`Hello ${this.name}`); // (*) }, sayBye() { super.say(`Bye ${this.name}`); // (*) } }; class User { constructor(name) { this.name = name; } } // copy the methods Object.assign(User.prototype, sayHiMixin); // now User can say hi new User('Dude').sayHi(); // Hello Dude!
-
We can create objects using functions, but that’s an old-school and frowned-upon way of making objects. Here're some of the reasons:
-
the object methods are created anew for each object. In the code above we create greet twice, and they do the same thing, but occupy different memory. It’s inefficient. Plus in order to modify or remove a method, we need to find all its occurrences in the code and fix them.
-
The main issue is that if we have many functions, then all of them are stored in memory. That’s not good. Because functions may be big, and we may need only one of them at a time.
-
That's where the protoType chain comes in handy, as it allows us to create a function once (in memory), and then inherit it in other objects -> which is done using
Object.create()function User(name) { const user = Object.create(sayHi); // (functions) is the prototype object and we use (Object.create) to set the prototype manually user.name = name; } function sayHi() { alert(this.name); } new User('John').sayHi(); // John
-
-
Another problem with functions is that they use the
[[Environment]]property to store the outer lexical environment reference. That’s needed to access outer variables. But here we don’t need it at all.
-
-
if you want a class method to be automatically called when an instance of the class is created, then you should put this method in the
constructorfunctionclass User { constructor(name) { this.name = name; this.sayHi(); // automatically called } sayHi() { alert(this.name); } } new User('John'); // John
-
in old syntax if we didn't use the word "new", then this keyword will point to the
windowobject -
Is it possible to create functions
AandBso thatnew A() == new B()?- Yes, it’s possible. If a function returns an object then
newreturns it instead ofthis.
let obj = {}; function A() { return obj; } function B() { return obj; } alert(new A() == new B()); // true
- Yes, it’s possible. If a function returns an object then
-
Values
nullandundefinedhave no object wrappers (are not Objects like other types) -
the
prototypalnature of JavaScript means that when an object can't find a property in itself, it will look up theprototypechain to see if the property is defined on the object'sprototype. If it can't find the property in theprototypeeither, it will look up theprototypechain again, until it reaches the end of theprototypechain. If it still can't find the property, it will returnundefined. -
When you use event listeners inside a class, you need to bind the
thiskeyword to the class instance, because thethiskeyword will point to the element that triggered the event, not the class instance.class Counter { constructor() { this.count = 0; this.btn = document.querySelector('button'); // this.btn.addEventListener('click', this.increment); // ❌ this.btn.addEventListener('click', this.increment.bind(this)); } increment() { this.count++; console.log(this.count); } } new Counter();
-
Losing
thisin (object / classes) methodsfunction User(name, age) { this.name = name; this.age = age; } User.prototype.sayHi = function () { alert(this.name); // works }; // losing due to Arrow function (lexical this) User.prototype.sayHello = () => { alert(this.name); // undefined }; // losing due to nested function User.prototype.sayBye = function () { function nested() { alert(this.name); // undefined } nested(); }; const user = new User('John', 30); user.sayHi(); // John ✅ user.sayHello(); // undefined ❌ user.sayBye(); // undefined ❌




















