Today I wanted to take a look at creating Javascript class instances and recreating the class relationships from the backend (database, models, etc.) on the frontend (Javascript in our case). I'm a student, so I'm still learning the ins and outs of Javascript, and what's worse I'm coming to it from Ruby. Don't get me wrong, I've been super excited to get to JS the whole time. It's such a big part of how we experience the internet (and it's got all the fun flashy stuff), but there's just something about the simplicity and intuitiveness of Ruby that I'm really missing right now trying to learn JS.
One of the big adjustments for me was being separated from the backend and having to mirror its structure and objects in a way that you just never have to think about when you're using something like Ruby on Rails. So am I writing this mostly for my own benefit as an attempt to hammer this into my brain and prevent future headaches? ...Maybe, but hopefully this can also make it easier for y'all the first time around.
So where do we start? There's no database schema or migrations to guide our associations, so we'll have to build our own. As usual, the magic happens in our class.
class Pizza {
}
Specifically the constructor method:
class Pizza {
constructor(pizza) {
this.name = pizza.name
this.toppings = pizza.toppings
this.deliciousness = pizza.deliciousness
}
}
What's going on there? I won't get too much into this
because it could be, and already is I'm sure, a full post itself, but more on that (...this?) here if you want a deeper look. For our purposes, suffice it to say that this
is going to be our new instance of the Pizza class. What we're doing in the constructor is telling our class how to build instances of itself and what attributes to give them. If you've worked with another Object Oriented language this should look familiar. In Ruby, for example, the initialize
method works essentially the same way.
So what's that pizza
argument? pizza
is what we've decided to call the object that's being passed in from our backend. Say, for example, we're getting a JSON object back from a pizza API. That object might look something like this:
[
{
"id": 1,
"name": "Margherita",
"deliciousness": 9,
"toppings": ["Mozzarella", "Tomato", "Basil"]
},
{
"id": 2,
"name": "Hawaiian",
"deliciousness": 3,
"toppings": ["Ham", "Pineapple"]
}
...
So we'd want to iterate through each of those objects and make new Pizza instances out of them. It's not very intuitive at first (why would we take one perfectly good object and then make a different identical object out of it?), but that's where our object relationships come in. Speaking of which, let's add some.
For our example, we'll say that Pizzas
belong to a Human
and a Human
can have many Pizzas
. That relationship would need to be reflected wherever we're getting our objects from (in this example, the database of our API), but so long as it is, we can represent it on the frontend by adding additional attributes in our constructors.
class Pizza {
constructor(pizza) {
this.name = pizza.name
this.toppings = pizza.toppings
this.deliciousness = pizza.deliciousness
this.human = pizza.human
}
}
class Human {
constructor(human) {
this.name = human.name
this.pizzas = human.pizzas
}
}
This code works; in our JSON object there would be something from the database indicating which human owns which pizza {"name": "Margherita", "human": {"name": "Cole"}}
and we can call our constructor method on a pizza object at any time to create a new class instance new Pizza(pizza)
. But there are a couple potential issues with that. The most common one for me had to do with class functions. I was getting errors left and right saying TypeError: <myFunc> is not a function
when it was clearly defined right there in my code.
Same classes as before, but let's create new instances and add a function this time.
// add function to Human class
class Human {
constructor(human) {
this.name = human.name
this.pizzas = human.pizzas
}
haveASlice(){
console.log("Pizza is the best!")
}
}
// our pretend backend objects
let pizza = {
"name": "Margherita",
"deliciousness": 9,
"toppings": ["Mozzarella", "Tomato", "Basil"],
"human": {"name": "Cole"}
}
let human = {"name": "Cole"}
// instantiating new class objects
let newPizza = new Pizza(pizza)
let newHuman = new Human(human)
Great! All set up, now we have new instances of each class and we can call the function haveASlice
on any instance of the Human class. So if we hop in the console and call
newHuman.haveASlice()
// returns
Pizza is the best!
Perfect, just like we expected! What about we go the round about way?
newPizza.human.haveASlice()
// returns
Uncaught TypeError: newPizza.human.haveASlice is not a function
What happened? newPizza.human
is a human just like newHuman
, right? In fact, they look exactly the same. The problem here is that even though they have the same attributes, newPizza.human
is just a regular old javascript object, where as newHuman
is an instance of a the Human class, which means that it has access to the functions defined in that class.
This was a big source of frustration in a recent project before I figured out the difference between the two and where in my code I still had regular objects floating around. The solution here is to always create instances, to associate them with the other classes they have relationships with in the constructor of that class, and to pay attention to the order that you're creating instances. For example, as long as I'm creating the Human instance first, I can do this:
class Pizza {
static all = []
constructor(pizza) {
this.name = pizza.name
this.toppings = pizza.toppings
this.deliciousness = pizza.deliciousness
this.human = Human.all.find(human => human.name === pizza.human.name)
Pizza.all.push(this)
}
}
class Human {
static all = []
constructor(human) {
this.name = human.name
this.pizzas = human.pizzas.map(pizza => new Pizza(pizza))
}
}
I know there might be a lot of new stuff there, but the idea is to create Pizza instances inside of the Human constructor and search for an existing Human instance when we're back in the Pizza constructor, so that the relationship between the classes is always being maintained by class instances. Here's a commented version:
class Pizza {
// initialize an array for all my instances to live
static all = []
constructor(pizza) {
this.name = pizza.name
this.toppings = pizza.toppings
this.deliciousness = pizza.deliciousness
// find an existing Human from the Human.all array below
this.human = Human.all.find(human => human.name === pizza.human.name)
// add 'this' (the new Pizza instance) to the Pizza.all array
Pizza.all.push(this)
}
}
class Human {
// initialize an array for all my instances to live
static all = []
constructor(human) {
this.name = human.name
// create new Pizza instances, collect them in an array
// and make that array of instances an attribute on the new Human instance
this.pizzas = human.pizzas.map(pizza => new Pizza(pizza))
}
}
I hope this has been helpful. Thanks for reading, go forth and create pizzas!
Top comments (0)