Repository navigation
- intro
- Static Methods
-
getsetGetter and Setters - Private Properties
- Private Properties with
getandset - Design: when to use
getandset extendssuper- Liskov's substitution principle
prototype- Class Variables
An alternative way to structure our code, Object Orientation. By understanding and using OO, we can design our code to be easier to read, understand and change.
Object Orientation grants us the ability to write code that structurally establishes the relationships between data and functions.
- Easier to Change
- Offers Better Data Control
- Easy to Replicate
- Improves Understanding by Adding Meaning
The class keyword is just syntactic sugar, or a nice abstraction, over JavaScript's existing prototypal object structure. no
functionkeyword is used for instance methods.
class Square {
constructor(sideLength) {
this.sideLength = sideLength;
}
// notice how the instance method doesn't have the `function` keyword
area() {
return this.sideLength * this.sideLength;
}
}
let square = new Square(5);
square; // => Square { sideLength: 5 }
square.sideLength; // => 5
square.area(); // => 25In class methods, this acts similar to Ruby's self keyword. this can be used to refer to properties of an instance, like name and age, or methods of an instance (this.sayName()).
You can also access the properties of an instance with:
square[`sideLenght`]
// => 5
square[`sideLenght`] = 13So to use variables to call properties.
Static methods are class level methods - they are not callable on instances of a class, only the class itself. These are often used in 'utility' class that encapsulate a set of methods but don't need to be made into instances. For example, we could write a CommonMath class that stores a series of math related methods.
For static methods, this references the class. This means that you can call a static method from within another static method of the same class using this.
class CommonMath {
static triple(number) {
return number * number * number;
}
static findHypotenuse(a, b) {
return Math.sqrt(a * a + b * b);
}
static doubleTriple(number) {
return this.triple(number) * 2
}
}
let num = CommonMath.triple(3);
num; // => 27
let c = CommonMath.findHypotenuse(3, 4);
c; // => 5This sort of class might be useful in many different situations, but we don't ever need an instance of it.
In modern JavaScript get has been introduced for methods which serve the specific purpose of retrieving data from an instance.
class Square {
constructor(sideLength) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength * this.sideLength;
}
}
let square = new Square(5);
square.sideLength; // => 5
square.area; // => 25If you try to use this.area(), you'll receive a TypeError. area is no longer considered a function!
//BAD PRACTICE!
class Square {
constructor(sideLength) {
this.sideLength = sideLength;
this.area = sideLength * sideLength;
}
}This is valid code, but what we've done is load our constructor with more calculations.
The main benefit to using get is that your area calculation isn't actually run until it is accessed.
If included in the constructor, an expensive process will be called every time a new instance of a class is created.
Since properties can change, any values dependent on them should be calculated based on the current property values, otherwise we will run in to issues like this:
//also:
class Square {
constructor(sideLength) {
this.sideLength = sideLength;
this.area = sideLength * sideLength;
}
}
let square = new Square(5);
square.area; // => 25
square.sideLength = 10;
square.area; // => 25class Square {
constructor(sideLength) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength * this.sideLength;
}
}
square.sideLength = 10;
square.area; // => 100We can change area by changing sideLenght however we can't assign area a new value. To make area fully act like a real property, we create both get and set methods for it:
class Square {
constructor(sideLength) {
this.sideLength = sideLength;
}
get area() {
return this.sideLength * this.sideLength;
}
set area(newArea) {
this.sideLength = Math.sqrt(newArea);
}
}
let square = new Square(5);
square.sideLength; // => 5
square.area; // => 25
square.area = 64;
square.sideLength; // => 8
square.area; // => 64area became a pseudo-property.
From the outside, it looks like a property is being set, but behind the scenes, we can define what we want to happen, including applying conditional statements:
set area(newArea) {
if (newArea > 0) {
this.sideLength = Math.sqrt(newArea)
} else {
console.warn("Area cannot be less than 0");
}
}All properties are accessible from outside an instance, as we see with square.sideLength, as well as from within class methods (this.sideLength).
class Transaction {
constructor(amount, date, memo) {
this.amount = amount;
this.date = date;
this.memo = memo;
}
}The date, amount and memo properties represent fixed values for each instance when a Transaction instance is created and probably shouldn't be altered. However, it is still possible to change these properties.
there is no official way to make a property private - all class and object properties are exposed as we see above. One common convention, however, is to include an underscore at the beginning of the property name to indicate those properties are not intended to be accessed from outside the class:
class Transaction {
constructor(amount, date, memo) {
this._amount = amount;
this._date = date;
this._memo = memo;
}
}Implementing private properties is planned in future versions of JavaScript, and will use a # symbol to indicate a property is private.
You may remember, though that in JavaScript, properties are exposed. That is, any property can be reassigned from outside. Here is where get and set really shine. With get and set, we can define the 'public' facing methods for updating a 'private' property.
In this Student class, we've set up a pseudo-property, firstName, which refers to a 'private' property _firstName:
class Student {
constructor(firstName, lastName) {
this._firstName = this.sanitize(firstName);
this._lastName = this.sanitize(lastName);
}
get firstName() {
return this.capitalize(this._firstName);
}
set firstName(firstName) {
this._firstName = this.sanitize(firstName);
}
capitalize(string) {
// capitalizes first letter
return string.charAt(0).toUpperCase() + string.slice(1);
}
sanitize(string) {
// removes any non alpha-numeric characters except dash and single quotes (apostrophes)
return string.replace(/[^A-Za-z0-9-']+/g, '');
}
}
let student = new Student('Carr@ol-Ann', ')Freel*ing');
student; // => Student { _firstName: 'Carrol-Ann', _lastName: 'Freeling' }
student.firstName = 'Hea@)@(!$)ther';
student.firstName; // => 'Heather'normal instance methods can do everything that get and set can do.
All class methods and properties are exposed for use 'publicly'. Using get and set is purely for design purposes to produce better organized, easier to understand classes.
We can use get and set whenever we are handling input or output of a class. We are, in essence, creating the public interface of the class.
get and set methods are the ways in which other classes and code should utilize this class.
Using this design, all remaining methods can be considered private. They don't deal with input and output; they are only used internally as helper methods.
Using a setter, you can call a function each time the value of a pseudo-property is changed, making sure all data on a class instance is consistent. Using a getter, you can return a computed value as though it is a property!
extends is used in class declarations to create a class which is a child of another class.
class Pet {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}!`
}
}
class Dog extends Pet {
// inherits constructor from Pet
}
class Cat extends Pet {
// inherits constructor from Pet
}
class Bird extends Pet {
// inherits constructor from Pet
fly() {
return `${this.name} flies away!`
}
}
let dog = new Dog("Shadow", "woof");
let cat = new Cat("Missy", "meow");
let bird = new Bird("Tiki", "squawk");
dog.speak(); // Shadow says woof!
cat.speak(); // Missy says meow!
bird.speak(); // Tiki says squawk!
bird.fly(); // Tiki flies away!super allows a child class to access a parent's constructor from within a child's constructor. It also allows a child class to access methods and properties from a parent class, but as most of these are already inherited, this is only useful when modifying static methods from the parent class.
class Pet {
constructor(name) {
this._name = name;
this._owner = null;
}
get name() {
return this._name;
}
get owner() {
return this._owner;
}
set owner(owner) {
this._owner = owner;
}
get speak() {
return `${this.name} speaks.`;
}
}
// Inherits from Pet
class Dog extends Pet {
constructor(name, breed) {
super(name); /* new */
this.breed = breed;
}
}
let creature = new Pet('The Thing');
let dog = new Dog('Spot', 'Foxhound');
dog;
// => Dog { _name: 'Spot', _owner: null, breed: 'Foxhound' }To understand why we use super above think of how you would have told otherwise to send the name variable to all of those methods inherited that use name in Pet.
super allows you to access anything in the parent class.
In a child class constructor, super is used as a method and calls the parent class constructor before continuing with the child. This lets us extend a parent's constructor inside a child. If we need to define custom behavior in a child constructor, we can do so without having to override or ignore the parent.
Outside of the constructor, the super keyword is also used, but this time, as an object not as a function.
In this case below, instance methods and properties are already inherited, so using super.owner will be the same as using this.owner:
// Inherits from Pet
class Dog extends Pet {
constructor(name, breed) {
super(name); /* new */
this._breed = breed;
}
get breed() {
return this._breed;
}
get info() {
if (super.owner) {
return `${this.name} is a ${this.breed} owned by ${super.owner}`;
}
return `${this.name} is a ${this.breed}`;
}
}
let charlie = new Dog('Charlie B. Barkin', 'Mutt');
dog.info;
// => 'Charlie B. Barkin is a Mutt'
let lady = new Dog('Lady', 'Cocker Spaniel');
lady.owner = 'Darling Dear';
lady.info;
// => 'Lady is a Cocker Spaniel owned by Darling Dear'Using super as an object is useful in situations where a parent class contains a static method that we want to expand on in a child class:
class Pet {
constructor(name) {
this.name = name;
this._owner = null;
}
get owner() {
return this._owner;
}
set owner(owner) {
this._owner = owner;
}
static definition() {
return `A pet is an animal kept primarily for a person's company.`;
}
}
// Inherits from Pet
class Dog extends Pet {
constructor(name, breed) {
super(name);
this.breed = breed;
}
static definition() {
return (
super.definition() + ' Dogs are one of the most common types of pets.'
);
}
}
let creature = new Pet('The Thing');
let dog = new Dog('Spot', 'foxhound');
Pet.definition();
Dog.definition();In Dog, we are able to use super.definition() to access that static method, then add to it, in this case, extending the definition to specifically reference dogs.
So super is mainly used to expand in a child class the definitions of a parent class.
JavaScript provide super for directly working with a parent class constructor and inherited methods.
Liskov's substitution principle: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. In terms to JavaScript inheritance, an instance of a parent class should be replaceable with an instance of a child class.
Properties and methods that exist on the parent will never be modified in any child. Child classes can expand upon what they inherited, adding methods or extra properties, do not modify what they inherited.
If you need to modify a child's inherited properties or methods, why are we inheriting them in the first place?
The benefit of following this principle is that no matter how complicated inheritance gets, you can always assume that whatever a parent class has, a child class will have too. Looking at a parent class should give us some insight into the functionality of any children, grandchildren, great grandchildren, etc...
A child class may include more properties or use the data differently. If you do find that a child class needs to overwrite a method or property it inherited from its parent, consider other options - perhaps don't inherit at all.
In general, when dealing with inheritance, the fewer levels of inheritance, the better. If you've got great grandparent, grandparent, parent and child classes, it can be difficult to figure out which class contributes what to a child. It also makes our code more difficult to change. You may not be able to modify code on a grandparent class without fundamentally changing how a parent or child class functions. Too much inheritance can make our code inflexible.
Languages that allow you to "extend what's already there" are Prototypal object systems such as languages like C, Lisp, Self, and JavaScript (natively).
- Class model: When we say "tree", our minds build a foggy rough idea of what all trees are like. There is a something defining tree-ness. (Plato)
- Prototypal model: Everything that is, is like a something else with more specification added (Aristotele)
Creating instances from a function (Constructor functions), when we invoke the constructor function with new before it, you can imagine an imaginary JavaScript Object being copied for use in the User function, the freshly-created, new context is changed.
Constructor functions creations identify their constructor as the constructor name.
The type of (typeof) its prototype attribute is Object.
function User(name, email) {
this.name = name;
this.email = email;
this.sayHello = function() {
console.log(`Hello everybody, my name is ${this.name} whom you've been
mailing at ${this.email}!`);
};
}
let lauren = new User('lauren', 'lauren@example.com');
lauren.name //=> [6] "Lauren"
lauren.sayHello(); //=> Hello everybody, my name is lauren whom you've been mailing at lauren@example.com!> typeof lauren
'object'
> lauren.constructor
[Function: User]Properties vary from instance to instance, but functions are always the same we should not instantiate them for each instance. That is why the above method to create instances is not efficient.
We would like to tell all instances of User that they have a shared place to find methods. That place is called the "prototype."
function User(name, email) {
this.name = name;
this.email = email;
}
User.prototype.sayHello = function() {
console.log(`Hello everybody, my name is ${this.name}`);
};
let sarah = new User('sarah', 'sarah@example.com');
let lauren = new User('Lauren', 'lauren@example.com');
sarah.sayHello(); //=> // "Hello everybody, my name is sarah!"
lauren.sayHello(); //=> // "Hello everybody, my name is Lauren!"
User.prototype;
// {sayHello: ƒ, constructor: ƒ}
typeof User.prototype;
// objectTo prove the efficiency of sharing methods via prototype:
lauren.sayHello === sarah.sayHello; //=> trueThe following code will create only 1 definition of razzle()
function Wizard(name) { this.name = name }
["radegast", "gandalf", "harry potter", "merlin"].map( n => new Wizard(n.charAt(0).toUpperCase() + n.slice(1) ))
Wizard.prototype.razzle = function() {
console.log(`${this.name} razzles you!`)
}These days, most OO JS will be written using class and the ES2016 standard, but you will likely encounter and might well be interviewed on questions around the Prototypal model.
class Clax {
varinst = 423;
static variClass() {
return 42
};
};Static function/variable is accessible from the Class
Clax.variClass();
//=> 42You can also assign Class variables after the Class has been defined
Clax.foo = 'jees';
Clax.foo //now accessible from the Class
//=> "jees"c = new Clax;
c.varInst
=> 423
c.foo
=> undefined
Clax.varInst
=> undefinedFrom within a class method that variable foo can be accessed as this.constructor.foo (or Clax.foo).
These class properties (foo) would not usually be accessible from to the class instance. i.e. Clax.foo gives 'bar' but new Clax().foo is undefined
If you want to also have access to your class variable from an instance, you'll have to additionally define a getter:
class Clax {
get foo() {
return this.constructor.foo;
}
}
Clax.foo = 'jees';To get all the instances of a Class you can create a static array and store it on your constructor function:
MyClass.allInstances = [];
MyClass.allInstances.push(this);