Time: 1 hour (plus however long you want to play)
JS classes are not too dissimilar from classes in Ruby or Python. If you're familiar with object-oriented programming, you'll see some structures that look pretty familiar.
- The constructor function is called
constructor
... so that's easy. This function runs when a new instance is created and initializes any instance variables. this
is used to tell an object (in this case a class instance) to refer to itself -- not unlikeself
in some other languages.extends
allows classes to inherit from a parent class;super
can be used within a child class to refer to the parent.- Method syntax can vary, but all methods in these examples are defined with the following pattern:
methodName (arguments) {
// method logic goes here
}
JS classes can accept positional arguments just fine:
class Human {
constructor(name, age) {
this.name = name
this.age = age
}
}
const jeremy = new Human('Jeremy', 666)
// jeremy.name will return "Jeremy"; jeremy.age will return 666
The classes in this repo, however, are designed to accept a single object as an argument on instantiation -- this is a pattern used in React, and the object is often referred to as "props," which is short for "properties." (In practice, it's not too unlike Python's kwargs
.) Take a look at the above, refactored to accept props:
class Human {
constructor(props) {
this.name = props.name
this.age = props.age
}
}
const jeremy = new Human({name: 'Jeremy', age: 666})
// jeremy.name will return "Jeremy"; jeremy.age will return 666
You can use either method for general JS OOP, but get used to seeing props
in React.
The files in this repo are designed as modular classes that can be imported into a Node environment. Animal
is the parent class, and child classes are contained in the species
directory. (There is also a list of built-in instances in instances.js
.) Clone this repo and click around in VSCode -- look for at least one example of each concept mentioned above, and try to wrap your head around it.
The best way to play with JS classes is to use the Node REPL, which allows us to enter expressions line by line and see the results. In this repository, index.js
will start a REPL with all of the classes you see in these files loaded by default. In your terminal, type:
node index.js
You should see a list of all the classes exported by classes.js
and the instances exported by instances.js
. Now you have animals to play with! Type animals
and you should see all of the current animal instances listed in an array. Type an individual animal's name and you should be able to access the individual object. Type a class name, and you should see that your REPL recognizes it as a function. Now do some stuff!
- Make a new animal instance of whatever type you like. (Check
instances.js
for hints.) - Look at the methods defined for
Animal
and for each subclass. Make the animals do stuff! Wallow! Eat! Charge! Make friends! Try stuff! Can Laura eat'a paper bag'
? Can Perry charge'a truck'
? Can Leo fetch'the paper'
? Can Spike make friends with'a fence'
? Can Lawrence eat Pia??? Zoiks! - Spike is a very friendly dog. Have him try to make friends with everybody:
animals.forEach(animal => Spike.makeFriends(animal))
- Perry is a very angry bull. Have him try to
charge
everybody by altering the code above. - Do whatever! Try to break it.
You can type
.reload
in the REPL to load any changes you've made to these files without restarting the REPL.
Create a new subclass of Animal
in a new file in the species
directory. Make it whatever you like. Give it some special properties or methods or both. Import the file in your Node shell (example: const Duck = require('./species/Duck)
) and create a new instance of your Duck
or Rabbit
or whatever. See if it can eat some stuff and make some friends!
You can type
.reloadClasses
in the REPL to load changes to your class definitions only while preserving the existing animal instances. Watch out: Existing animals will still use the original class definitions!
If you're comfortable here, then you know slightly more than you need to know about JS OOP in order to get started with class-based components in React. Great!
To dig in deeper, start with these study questions. Try to find your own explanation and compare ideas with a peer before revealing the answer!
1) What's going on in instances.js?
Line 1: The Animal
class and a number of child classes are imported from classes.js
via require
. (They are actually imported into that file from their individual sources.)
Lines 2 - 11: The imported classes are used to create new instances of the various imported classes, and each of these is stored as a variable with the same name as the critter.
Line 12: Each of the above instances is placed into a new array. (Note: This will place a reference to the original object in the array. Monique
and animals[0]
will now both point to the same object.)
Line 13: The individual instances and the array are exported from this file, meaning other files can access them via require
.
2) What happens when an Animal eats?
The .eat(food)
method of the Animal
class follows the folowwing steps:
-
Invokes
this.isHungry()
, which returns a boolean based on whether the animal has fewer than 4 items in thethis.stuffInBelly
array. -
If the above returns
true
, thefood
argument is placed inthis.stuffInBelly
using.push
. (This is true regardless of what datatype is provided forfood
! ๐ฌ ) The animals name and food eaten are logged to the console. -
If the animal is not hungry, the animal's name is logged to the console along with a message saying it doesn't want to eat.
3) What is the .toString() method? Is it used anywhere?
The Animal
class' .toString()
method simply returns a string with the instance's name
property along with this.constructor.name
, which will be the name of the instance's class. (If a child class extends
the Animal
class, that child class' name will be provided here.)
.toString()
is not directly used in any of this code -- but he method is invoked any time an object is directly converted to a string, as in a literal with backticks and ${}
. This can be seen in the makeFriends()
method:
console.log(`${this} and ${newFriend} are now friends!`)
// produces output like "Pia the Chicken and Spike the Dog are now friends!"
If you comment out the .toString()
method in Animal
, re-import the objects, and direct two animals to make friends, you will see this instead:
[object Object] and [object Object] are now friends!
(Note: Many datatypes -- Number
, Array
, Object
and more -- actually have a default .toString()
method that is invoked in the same situations!)
4) What happens if Lawrence and Pia become friends?
Supposing this means we have entered Lawrence.makeFriends(Pia)
into the REPL, Lawrence
is an instance of Cat
and Pia
is an instance of Chicken
, both of which inherit from Animal
.
The .makeFriends(newFriend)
method of the Lawrence
object will be invoked. For the Cat
class, this method is inherited from Animal
, so the code for the method can be found in Animal.js
.
This method first checks whether the newFriend
argument is an instance of Animal
or a child class of Animal
by checking newFriend.constructor
and newFriend.__proto__.__proto__.constructor
, which returns the parent class. So no, in this system, an Animal
cannot be friends with 25
or "a paper bag"
, only with another Animal
.
Then the method checks whether the two instances are already friends by determining whether a reference to newFriend
is found in the this.friends
array.
If the two conditions are met, a reference to newFriend
is added to this.friends
-- and a reference to this
(which will be the current object, in this case Lawrence
) will be added to newFriend.friends
. Then an announcement of the new friendship is logged.
If the conditions aren't met, the method checks why. If this.friends.includes(newFriend)
, then the log explains that the animals are already pals. Otherwise, the method must have failed because of newFriend
's type -- the log explains that the animal cannot be friends with a newFriend.constructor.name
, which will output newFriend
's type!