Classes are this really cool way of creating repeatable objects that have similar properties! What an understatement!


Alright, let's go.

class Animal {
    talk() {
        console.log("Moooo");
    }
}

async function main() {
    let animal1 = new Animal;

    animal1.talk();
}

main();

Outputs

john.celoria@MacBook-Pro-2 hello-world % node .
Moooo
john.celoria@MacBook-Pro-2 hello-world % 

Seems kind of over the top John.... Well ya, you could do this with just one line, but you gotta think about a big picture. What if you had hundreds of animals of different types, and then thousands of each type??

First, when you declare a new class you capitalize the first letter of its name. Makes it easier to read your code. Just do it.

Next up, in the above, we declare a method, talk(). Looks just like a function, cause it is! It's a function that's going to be specific to the instance of the Animal object you create. So let's discuss that.

let cow = new Animal; this instantiates a new Animal object and gives it a variable name of cow. You can for sure make hundreds of them!!! Each with a different variable name. Which is cool.

But, for real, this is pointless right? Well, no. Let's do something more interesting here.

/**
 * Base animal class that can be extended to other species!
 */
class Animal {
    constructor(name = "Jimbo", saysWhat = "mooo") {
        this.name = name;
        this.saysWhat = saysWhat;
    }
    
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}`);
    }
}

async function main() {

    const animal1 = new Animal();
    const animal2 = new Animal("Johnny C.", "bring me more coffee");

    animal1.talk();
    animal2.talk();
}

main();

Well that's fancy!!!! We've still got our talk() method, but now, when it's called, it's reference some variables specific to its instance of Animal. That's what the this keyword is. It means that every instance of Animal will have a name field, and a saysWhat field, but when the instance's talk() method is called, the instance will reference its own name and saysWhat variable.

Now that constructor() is important. It let's you set those fields up when instantiating a new instance of the Animal class! In the constructor we defined some default values, Jimbo and mooo. When you instantiate a new Animal you can override those defaults by passing in new values (in order!).

One more thing that you haven't seen before. See that cool lookin comment stuff above the class definition? That's a good place to toss information about the class itself. Because, it will show up in most editors as a tooltip, not to mention it'll remind you what you were doing when you come back to this code!

Screen-Shot-2020-09-17-at-4.50.08-PM

Let's see what doing that to the talk() method does.
Screen-Shot-2020-09-17-at-4.50.58-PM

Pretty similar right? Welcome to what should be a requirement in every bit of code you write! Documentation!!!


Let's extend your knowledge, with extend!

/**
 * Base animal class that can be extended to other species!
 */
class Animal {
    constructor(name = "Jimbo", saysWhat = "mooo") {
        this.name = name;
        this.saysWhat = saysWhat;
    }

    /**
     * This is what the animal says. It's pretty cool...
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}`);
    }
}

class Mammal extends Animal {
    constructor(name = "Jimbo", saysWhat = "mooo", legs = 0) {
        super(name, saysWhat);
        this.legs = legs;
    }

    /**
     * Talk, but also how many legs eh?
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}, and has ${this.legs} legs`);
    }
}

async function main() {

    const animal1 = new Animal();
    const animal2 = new Animal("Johnny C.", "bring me more coffee");
    const cat = new Mammal("Cool cat", "meow", 4);

    animal1.talk();
    animal2.talk();
    cat.talk();
}

main();

Cool things here! So we've got a brand new class that we've made, that's extending the Animal class. So those things that all Animal instances can do, now all Mammal instances can do too. We've added on a new field for each Mammal calls legs. So, in our constructor, we've got to pass legs in now too. In the constructor, we use a method word called super() to "pass up" the needed fields for the previous constructor. Then, since legs only apply to Mammals, we set that explicitly here too.

Next thing happening up there is that we override the talk() method! If we didn't have that there, then cat.talk(); would do the same thing as Animals, since Mammals inherit that method. We want to show off that Mammals have legs, so we override talk() to include that.

Let's extend again!!!

I'm getting tired of having to put my parameters in, in order. I'm going to go back to my constructor()s and set them up to expect a JSON object, and then access the fields of that body instead. I've got to move my default values for those fields somewhere else though. Promise it's tidier.

/**
 * Base animal class that can be extended to other species!
 */
class Animal {
    constructor(data = {
        name: "Jimbo",
        saysWhat: "mooo"
    }) {
        this.name = data.name;
        this.saysWhat = data.saysWhat;
    }

    /**
     * This is what the animal says. It's pretty cool...
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}`);
    }
}

class Mammal extends Animal {
    constructor(data = {
        name: undefined,
        saysWhat: undefined,
        legs: 0
    }) {
        super(data);
        this.legs = data.legs;
    }

    /**
     * Talk, but also how many legs eh?
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}, and has ${this.legs} legs`);
    }
}

async function main() {

    const animal1 = new Animal();
    const animal2 = new Animal({
        name: "Johnny C.",
        saysWhat: "bring me more coffee"
    });
    const cat = new Mammal({ 
        name: "Cool cat", 
        saysWhat: "meow", 
        legs: 4 
    });

    animal1.talk();
    animal2.talk();
    cat.talk();
}

main();

Alright let's talk. Back to Animal. The constructor there expects an object, and just incase, we're ensuring that there's a default object set to nothing in the constructor. constructor(data = {name: "Jimbo", saysWhat: "mooo"}); <-- this is some cool shorthand to say "Here's a default for the inbound object, but override that if the developer actually passes something in".

So we've got Animal using an object, and now we extend that to Mammal. It's super expects an object, so we pass up the whole object we got in our constructor, then expect an extra item for legs. If there's no legs in the object, we default to 0, which just makes us sad. Well I guess manatees are mammals without legs, so that's fine, right?

Extend again!!

/**
 * Base animal class that can be extended to other species!
 */
class Animal {
    constructor(data = {
        name: "Jimbo",
        saysWhat: "mooo"
    }) {
        this.name = data.name;
        this.saysWhat = data.saysWhat;
    }

    /**
     * This is what the animal says. It's pretty cool...
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}`);
    }
}

class Mammal extends Animal {
    constructor(data = {
        name: undefined,
        saysWhat: undefined,
        legs: 0
    }) {
        super(data);
        this.legs = data.legs;
    }

    /**
     * Talk, but also how many legs eh?
     */
    talk() {
        console.log(`${this.name} says, ${this.saysWhat}, and has ${this.legs} legs`);
    }
}

/**
 * This is a specific class for Dogs!
 */
class Dog extends Mammal {
    constructor(data = {
        name: "Rover",
        saysWhat: "bark!",
        color: "brown",
        breed: "Australian Cattle Dog",
        legs: 4
    }) {
        super(data);
        this.color = data.color;
        this.breed = data.breed;
    }

    // we'll use the same talk() that we inherited

    /**
     * Bork bork!
     */
    bark() {
        console.log(`I'm a beautiful, ${this.color}, ${this.breed}`)
    }
}

async function main() {

    const animal1 = new Animal();
    const animal2 = new Animal({
        name: "Johnny C.",
        saysWhat: "bring me more coffee"
    });
    const cat = new Mammal({ 
        name: "Cool cat", 
        saysWhat: "meow", 
        legs: 4 
    });

    const dog1 = new Dog();
    const dog2 = new Dog({
        color: "black",
        breed: "Labrador Retreiver"
    });
    const dog3 = new Dog({
        name: "Calamity Jane",
        saysWhat: "I love you",
        color: "cookies and cream",
        breed: "Super Mutt"
    })

    animal1.talk();
    animal2.talk();
    cat.talk();

    dog1.talk();
    dog1.bark();

    dog2.talk();
    dog2.bark();

    dog3.talk();
    dog3.bark();
}

main();

Alright! There's tons going on here, but I want you to try and understand on your own. When are these instances of the various classes using inherited methods? How many layers upward are some of these inheriting from??

Next stop, APIs...