DEV Community

cole-flournoy
cole-flournoy

Posted on • Updated on

A Beginner's Guide to Dealing With Classes and Object Relationships in Javascript

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 {

}
Enter fullscreen mode Exit fullscreen mode

Specifically the constructor method:

class Pizza {
  constructor(pizza) {
    this.name = pizza.name
    this.toppings = pizza.toppings
    this.deliciousness = pizza.deliciousness
  }
}
Enter fullscreen mode Exit fullscreen mode

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"]
  }
  ...      
Enter fullscreen mode Exit fullscreen mode

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
  }
}

Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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))
  }
}

Enter fullscreen mode Exit fullscreen mode

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))
  }
}
Enter fullscreen mode Exit fullscreen mode

I hope this has been helpful. Thanks for reading, go forth and create pizzas!

Discussion (0)