- JS classes are like syntactic sugar, not the same as classes of other strongly typed languages.
- Only adds syntax-wrapping to make it familiar for devs coming from other languages.
- classes are a special type of functions behind the scenes, hence can be written as class expression as well as class declaration.
## class expression:
const Person = class {
}
## class declaration:
class Person {
constructor(fName, bYear){
this.fName = fName;
this.bYear = bYear;
}
calcAge(){
console.log(2024 - this.bYear);
}
}
- constructor is a method of this class. Pass values for properties to have in objects created using this fn.
- then set the properties of the object using this.xxx = xxx;
- On using 'new' operator, this constructor will be called automatically and return a new object which will be stored in LHS variable as shown below.
Ex. const ronald = new Person('ronald',1975); // Person { fName: 'ronald', bYear: 1975 }
- Methods are written outside the constructor fn and will be added to the prototype property of the object which can be verified using devConsole.
Ex. ronald.calcAge(); // 49
ronald.__proto__ === Person.prototype; // true
- No commas need to be added while adding multiple methods below the constructor fn inside the class.
## Hence, the above syntax works same as constructor fn syntax but with a familiar syntax of strongly typed class based languages.
## Adding a fn explicitly to the prototype:
Person.prototype.greet = function(){
console.log(`Hey ${this.fName}`);
}
ronald.greet(); // 'Hey ronald'
Impt Points:
- Fn declarations are hoisted while Class declarations are NOT hoisted.
- Also first class citizen just like Fns i.e can be passed-to & returned-from fns.
- Body of class is always executed in strict mode whether we activate the strict mode or not.
- Classes make the code look cleaner, with reduced character noise provided you know how its implemented under the hood. ** To become an expert in JS, you need to understand the intricate language implementation details like with classes.
Accessor Properties: Getters & Setters i.e fns that get & set the value. But on the outside, they still look like regular properties.
Normal Properties are called Data Properties.
- Getters & Settters are common to all objects in JS i.e every object can have getter and setter property. These getter-setter are called as accessor properties, while normal properties are called as data properties.
- Getter & setter are fns that get and set a value, from outside look like normal properties.
const account = {
owner: 'jonas',
movements: [200,300,100,500],
get latest(){
// will return an array with last value. Hence, use pop to get the value.
return this.movements.slice(-1).pop();
},
set latest(mov){
this.movements.push(mov);
}
}
account.latest; // 500
account.latest = 50;
account.latest; // 50
Just like above, classes also support the getter-setter methods but acccessed like using a property syntax.
These are very useful for data validation.
Static Methods
Ex. Array.from() = Converts array-like structure to array.
Array.from(document.querySelector('h1'));
Array.from(document.querySelectorAll('h1'));
Ex. .from is attached to the Array construcutor, not to the prototype property of the constructor. Hence all arrays don't inherit this fn.
[1,2,3].from(); // .from is not a function
Ex. Number.parseFloat(12) is a static method on Number constructor, not available on number variables.
Creating a static method.
// Static methods are not inherited. They are not added to prototype.
className.fnName = function(){
console.log(this); // Entire constructor() which is calling the method
console.log("JS is awesome")
};
className.fnName();
// Rule = whatever object is calling the method, 'this' points to that object inside the fn. Hence its simply the entire constructor() above.
//Inside class, we need to use static keyword for adding a static method.
static fnName = function(){
console.log(this); // can point to the entire class defn
console.log("JS is awesome")
};
// Static methods and instance methods will be different from each other.
// instance methods will be prototype, hence all instances can have access to them
Object.create():
Manually used to set prototype of our object to any object that we want.
Will be used to implement inheritance b/w classes.
Prototypal Inheritance Implemented using this fn.
Object.create returns an empty object.
Works in a different way in which constructor fns and classes work.
Still there is idea of prototypal inheritance, even without involvement of 'prototype', 'constructor()', 'new' operator.
const PersonProto = {
// This method will be looked up using __proto__ link
calcAge(){
console.log(2024 - this.bYear);
}
};
// baba will be created, with its prototype set to PersonProto object.
const baba = Object.create(PersonProto);
baba;
baba.name = 'Roger';
baba.bYear = '2000';
baba.calcAge();
Constructor Fn --(.prototype)--> Person.prototype
Object Instance --(proto)--> Person.prototype
Works just like it worked for fn constructors or in classes
No need of constructor() or .prototype property to achieve this goal.
const PersonProto = {
// This method will be looked up using __proto__ link
calcAge(){
console.log(2024 - this.bYear);
},
// Noting special with init name, its a normal fn here.
// This has nothing to with ES6 constructor()
// Manual way of initialzing an object.
init(fName, bYear){
this.fName = fName;
this.bYear = bYear;
}
};
// baba will be created, with its prototype set to PersonProto object.
const baba = Object.create(PersonProto);
baba;
baba.name = 'Roger';
baba.bYear = '2000';
baba.calcAge();
baba.__proto__; // { calcAge: [Function: calcAge] }
baba.__proto__ === PersonProto; //true
const alice = Object.create(PersonProto);
alice.init("alice", 2000);
alice; // { fName: 'alice', bYear: 2000 }
Ways to create Prototypal Inhertitance:
Constructor Fn
ES6 Classes
Object.create
Inheritance between Classes using constructor():
All of these techniques allow object to lookup for methods on its prototype.
Real classes do not exist in JS.
const Person = function(firstName, bYear){
this.firstName = firstName;
this.bYear = bYear;
};
Person.prototype.calcAge = function(){
console.log(2024 - this.bYear);
};
const Student = function(firstName, bYear, course){
// This is the duplicate code, any change in Person won't be reflected here.
this.firstName = firstName;
this.bYear = bYear;
this.course = course;
};
Student.prototype.introduce = function(){
console.log(`My name is ${this.firstName} and I study ${this.course}`);
}
const matt = new Student("Matt", 2000, "CSE");
matt.introduce(); // 'My name is Matt and I study CSE'
Removing redundant code from above example:
const Person = function(firstName, bYear){
this.firstName = firstName;
this.bYear = bYear;
};
Person.prototype.calcAge = function(){
console.log(2024 - this.bYear);
};
const Student = function(firstName, bYear, course){
// Person(firstName, bYear); -> This doesn't work because we are calling it as a regular fn call. 'new' has to be used to call this fn constructor. This fn call is simply a regular fn call, in which 'this' is set 'undefined'. Hence, an error as it cannot set firstName on undefined.
// We want to set the 'this' inside this fn to be same as inside Person above.
Person.call(this, firstName, bYear);
this.course = course;
};
Student.prototype.introduce = function(){
console.log(`My name is ${this.firstName} and I study ${this.course}`);
}
const matt = new Student("Matt", 2000, "CSE");
matt.introduce(); // 'My name is Matt and I study CSE'
'new' makes a link automatically between object instance and its prototype via proto
Whole idea of inheritance is that child class can share behavior from parent classes up the prototype chain.
Prototype[Object.prototype] = null; // Sits on top of prototype chain.
const Person = function(firstName, bYear){
this.firstName = firstName;
this.bYear = bYear;
};
Person.prototype.calcAge = function(){
console.log(2024 - this.bYear);
};
const Student = function(firstName, bYear, course){
Person.call(this, firstName, bYear);
this.course = course;
};
// Student.prototype = Person.prototype; => This doesn't work because we won't get the prototype chain, rather we will get
// Constructor fn[i.e Person()] --------------> Person.prototype
// Constructor fn[i.e Student()] --------------> Person.prototype
// Object [Matt] __proto__: Student.prototype ---> Person.prototype
// Student.prototype manually linked for lookup to Person.prototype.
// This has to be done here and not after else Object.create will overwrite any of the existing methods like introduce() on it.
Student.prototype = Object.create(Person.prototype);
Student.prototype.introduce = function(){
console.log(`My name is ${this.firstName} and I study ${this.course}`);
}
const matt = new Student("Matt", 2000, "CSE");
matt.introduce(); // 'My name is Matt and I study CSE'
matt.calcAge(); // 24
matt.__proto__; // Person { introduce: [Function (anonymous)] }
matt.__proto__.__proto__; // { calcAge: [Function (anonymous)] }
matt.__proto__.__proto__.__proto__; // [Object: null prototype] {}
Student.prototype.constructor = Student; // [Function: Student]
matt instanceof Student; // true
matt instanceof Person; // true
matt instanceof Object; // true
class syntax hides a lot of behind the scene details.
Its just a layer of abstraction over constructor fns.
Mainly extend, super are needed to implement the same functionality of inheritance using classes which was established above using constructor fns.
extends - inheritance between classes, automatically sets prototype chain for us.
public field - similar to property defined in the constructor, available on created object
private field - not accessible outside of class, perfect for implementation of data privacy & encapsulation
static public field - available only on class
constructor() - automatically called by new operator whenever a new object from the class is created. Mandatory in parent class, can be omitted in child class if we want to have exact same name & same no of arguments in child class.
super():
its the constructor fn of the parent class, just pass-in the required arguments.
Always need to happen first, as this call is responsible for creating 'this' keyword in child-class.
Without doing this, we won't be able to access the 'this.course = course'
call to parent(super) class (necessary with extend). Needs to happen before accessing 'this' keyword in the constructor.
instance property - available on each created object, set based on input data of the constructor. fields are common for all the object, while these properties are unique for each object.
getter method - get a value out of an object by simply writing a property instead of a method.
setter method - use '_' to set a property with name as method, and also add getter.
static method - available only on class, cannot access instance properties nor methods, only static ones. Ex. only static public fields will be accessible inside static methods.
NOTE: Classes are:
- syyntactic sugar over constructor fns.
- not hoisted
- first class citizens just like fns.
- class body is always executed in strict mode