GLAB 308A.2.1 - Objects and Orcs


Learning Objectives

After this lab, learners will have demonstrated the ability to:

  • Use an array inside an object.
  • Iterate over an array that is within an object.
  • Use an object within an object.

    • Use an object within an object within an object.
    • Use an array within an object within an object within an object.
  • Use an array of objects.
  • Combine objects, arrays, and functions.
  • Create a class to define the blueprint for creating objects.
  • Add methods to a class.
  • Set properties on an instance of a class.
  • Make an instance of each class customizable.
  • Create methods to alter the properties of an instance.
  • Make a class inherit attributes from a "parent" class.
  • Create static properties for a class.
  • Create a "factory."

 CodeSandbox

This lab uses CodeSandbox as one of its tools.

If you are unfamiliar with CodeSandbox, or need a refresher, please visit our reference page on CodeSandbox for instructions on:

  • Creating an Account
  • Making a Sandbox
  • Navigating your Sandbox
  • Submitting a link to your Sandbox to Canvas

Instructions

  1. Create a Vanilla CodeSandbox and name it "Objects and Orcs."
  2. Follow along with the instructions below, adding to your index.js file.
  3. Submit the link to your CodeSandbox on Canvas when you are finished.

Deliverables

  • A link to a CodeSandbox that contains your "Objects and Orcs" game with no errors (comment things out if they do not work).

Objects and Orcs!

We'll be creating a simple adventuring game using the principles of Object-Oriented Programming and the JavaScript tools we're explored so far.

We will start with the basic concepts and structure, and then refine it with classes and other programming patterns.


Arrays within Objects

Let's model an adventurer who has belongings (a list):

const adventurer = {
	name: "Timothy",
	hitpoints: 10,
	belongings: ["sword", "potion", "Tums"]
}

Access all values in the adventurer.belongings array:

console.log(adventurer.belongings)

=> ["sword", "potion", "Tums"]

Access a specific item in the belongings array:

console.log(adventurer.belongings[0])

=> "sword"

Iterate Over an Array within an Object

for (let i=0; i < adventurer.belongings.length; i++) {
	console.log(adventurer.belongings[i]);
}

Object within an Object

Our adventurer now has a companion! Our companion, a bat, is an object with its own properties.

Add the companion object to the adventurer object:

const adventurer = {
	name: "Timothy",
	hitpoints: 10,
	belongings: ["sword", "potion", "Tums"],
	companion: {
		name: "Velma",
		type: "Bat"
	}
}

Access the companion object:

console.log(adventurer.companion)

=> { name: "Velma", type: "Bat" }

Access the companion's name:

console.log(adventurer.companion.name)

=> "Velma"

Access the companion's type:

console.log(adventurer.companion.type)

=> "Bat"


Object within an Object within an Object...

Velma the bat also has a companion, a magical parasite called Tim.

Let's add Tim to our data:

const adventurer = {
	name: Timothy,
	hitpoints: 10,
	belongings: ["sword", "potion", "Tums"],
	companion: {
		name: "Velma",
		type: "Bat",
		companion: {
			name: "Tim",
			type: "Parasite"
		}  
	}
}

What would you write to console.log Tim's type?


Array within an Object within an Object within an Object.....

Tim has a bag of holding and can carry an infinite number of belongings.

Let's add an array of belongings to Tim:

const adventurer = {
	name: 'Timothy',
	hitpoints: 10,
	belongings: ["sword", "potion", "Tums"],
	companion: {
		name: "Velma",
		type: "Bat",
		companion: {
			name: "Tim",
			type: "Parasite",
			belongings: ["SCUBA tank", "Rogan josh", "health insurance"]
		}  
	}
}

What would your write to console.log "health insurance"?

At this point, you're beginning to see the need for some different data structures (think classes!). We'll be looking at how to more efficiently produce these entities soon.


An Array of Objects

A common pattern you will start to see everywhere is an array of objects.

An array of objects can look like this:

const movies = [ { title: "Tokyo Story" },  { title: "Paul Blart: Mall Cop" }, { title: "L'Avventura" } ];

These objects have no names, they are just anonymous objects packed into an array.

You could reference them with indexes as usual:

console.log(movies[0]);

You could reference the properties by first asking for the index, then the property:

console.log(movies[0].title);

You could loop over the array and just print all of the titles:

for (let i=0; i < movies.length; i++) {
	console.log(movies[i].title);
}

Combining Objects, Arrays, and Functions

You can create a property for an object that is an array:

const foo = {
    someArray:[1,2,3]
};
foo.someArray[0]; // 1

You can create a property for an object that is an object:

const foo = {
    someObject: {
        someProperty: 'oh hai!'
    }
};
foo.someObject.someProperty; // oh hai!

You can create a property for an object that is a function (method):

const foo = {
    someMethod:()=>{
        console.log('oh hai');
    }
};

foo.someMethod();// logs 'oh hai!'

You can store an object in an array:

const foo = [{someProperty:'weee'}, 2, 3];

console.log(foo[0].someProperty);

You can store an array in an array:

const foo = [
    ["0,0", "0,1", "0,2"],
    ["1,0", "1,1", "1,2"],
    ["2,0", "2,1", "2,2"]
];

foo[1][2]; //1,2

You can store a function in an array:

const foo = [
    1,
    "hi",
    ()=>{
        console.log('fun');
    }
];

foo[2]();

Adding Classes

As we can see, we need to repetitively create new objects with the same attributes a lot. Imagine if we wanted to create a bunch of characters.

We'd need at least:

  • name
  • health
  • power
  • stamina

Imagine if we had 500 players! Would our current structure be okay?

  • What if the structure changed and we had to update them all individually?
  • What if we needed to upgrade the players?
	const player = {
	  name: 'Corey the Great',
	  health: 1000,
	  power: 1000,
	  stamina: 1000
	}

	const bigBadBoss = {
	  name: 'Menacio the Merciless',
	  health: 1000000000000000000,
	  power: 10000000000000000,
	  stamina: Infinity
	}

  ...

Would a function work?

const createEnemy = (nameIs, healthIs, powerIs, staminaIs) => {
  const newEnemy = {
    name: nameIs,
    health: healthIs,
    power: powerIs,
    stamina: staminaIs
  }
  return newEnemy
}

const createPlayer = (nameIs, healthIs, powerIs, staminaIs) => {
  const newPlayer = {
    name: nameIs,
    health: healthIs,
    power: powerIs,
    stamina: staminaIs
  }
  return newPlayer
}

Great! A function that returns objects. How can we create another one? How about copy pasting, then changing all the details? Typing it all from scratch? What if someone makes a typo with a key?

There is a better way, as you know! We can create a class, which will be a blueprint or template for similar objects. Not only can we add data, we can also include functionality.


Character Class

When creating a class, it's best practice to capitalize the first letter of the variable, so we know it's a class. This follows customs in other programming languages as well.

class Character {
  // going on an adventure
}

Now we can "instantiate" or create new objects using this class. We do this by adding the new keyword before calling the class name like a function.

const me = new Character();
const you = new Character();

console.log(me);
console.log(you);
console.log(typeof me);
console.log(typeof you);

Adding Methods to Character

Right now, our object doesn't do anything. Let's have it do some stuff.

class Character {
  greet () {
    console.log('Hi!');
  }
}

const me = new Character();
const you = new Character();

me.greet();
you.greet();

These methods can, of course, take parameters:

class Character {
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
}
const me = new Character();
const you = new Character();
me.greet('you');
you.greet('me');

We only had to update our code in one place, and then every instance of the class now has the updated functionality. Nice!

If we have multiple methods, don't put commas between them:

class Character {
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}

const me = new Character();
const you = new Character();
me.greet('bob');
me.walk();
you.greet('bob');
you.walk();

Setting Properties

If we log the instances of our class, we'll see they don't have any properties:

class Character {
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}

const me = new Character();
const you = new Character();
console.log(me);
console.log(you);

Let's add some properties with a constructor function:

class Character {
  constructor () {
    this.legs = 2;
    this.arms = 2;
    this.eyes = 'hazel';
    this.hair = 'gray';
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}
const me = new Character();
console.log(me);

constructor is a special function. Try misspelling constructor (ie constr) and see if you still get the same results.

Reserved Words in Javascript has some useful information on what words have special meaning like this.


Custom Constructors

Our world is very boring and weird if all of our people are exactly the same! We need a way to customize each object. Our constructor function can take params which we can use to alter the properties of the object instantiated. This allows us to customize each instance:

class Character {
  constructor (name, age, eyes, hair) {
    this.legs = 2;
    this.arms = 2;
    this.name = name;
    this.age = age;
    this.eyes = eyes;
    this.hair = hair;
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}

const me = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown');
console.log(me);

Creating Default Values

Sometimes, you want to create default values that can be overwritten.

There are two ways to write it:

  • Writing it in the constructor with an = is the newer way.
  • Using || is the older way and does work.

In both cases, values with that have default parameters should go at the end.

class Character {
  constructor (name, age, eyes, hair, lovesCats = false, lovesDogs) {
    this.legs = 2;
    this.arms = 2;
    this.name = name;
    this.age = age;
    this.eyes = eyes;
    this.hair = hair;
    this.lovesCats = lovesCats;
    this.lovesDogs = lovesDogs || false;
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}
const you = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown', true, true);
const me = new Character('Wendel the Wavy', 32, 'brown', 'wavy blonde');
console.log(me);
console.log(you);

Class Methods

We can of course, alter the properties of an instance, after it is created.

me.hair = 'supernova red';
console.log(me);

But it's a nice practice to define a method that will alter that for us. This has uses beyond just being "correct" when things get more complex.

class Character {
  constructor (name, age, eyes, hair, lovesCats = true, lovesDogs) {
    this.legs = 2;
    this.arms = 2;
    this.name = name;
    this.age = age;
    this.eyes = eyes;
    this.hair = hair;
    this.lovesCats = lovesCats;
    this.lovesDogs = lovesDogs || true;
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  setHair (hairColor) {
    this.hair = hairColor;
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}

const me = new Character('Wendel the Wavy', 32, 'brown', 'wavy blonde');
console.log(you);
you.setHair('red');
console.log(you);

This way, everything is done with methods that have predictable results on our objects. Other developers can quickly scan the class definition to determine what you'd like them to be able to do with the class.


Object Interactions

We can pass an object to another object to have them interact.

class Character {
  constructor (name, age, eyes, hair, lovesCats = false, lovesDogs) {
    this.legs = 2;
    this.arms = 2;
    this.name = name;
    this.age = age;
    this.eyes = eyes;
    this.hair = hair;
    this.lovesCats = lovesCats;
    this.lovesDogs = lovesDogs || false;
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  classyGreeting (otherClassyCharacter) {
    console.log('Greetings ' + otherClassyCharacter.name + '!');
  }
  setHair (hairColor) {
    this.hair = hairColor;
  }
  smite () {
    console.log('I smite thee you vile person!');
  }
}
const you = new Character('Cathy the Miraculous', 29, 'brown', 'locs of dark brown', true, true);
const me = new Character('Wendel the Wavy', 32, 'brown', 'wavy blonde');

me.classyGreeting(you);
you.classyGreeting(me);

Now we can see how classes work in this context, so how could we use our Character class to now create more classes, like Orcs, Dragons, Wizards, Sages, and Blood Elves?


Inheritance

Sometimes we want to have a "parent" class that will have some basic attributes that will be inherited by "child" classes.

Here is our parent class, but what if we have a superhero amongst us that has all our human attributes and more?

class Character {
  constructor (name, age, eyes, hair, lovesCats = true, lovesDogs) {
    this.legs = 2;
    this.arms = 2;
    this.name = name;
    this.age = age;
    this.eyes = eyes;
    this.hair = hair;
    this.lovesCats = lovesCats;
    this.lovesDogs = lovesDogs || true;
  }
  greet (otherCharacter) {
    console.log('Hi ' + otherCharacter + '!');
  }
  classyGreeting (otherClassyCharacter) {
    console.log('Greetings ' + otherClassyCharacter.name + '!');
  }
  setHair (hairColor) {
    this.hair = hairColor;
  }
  smite () {
    console.log('I smite thee you vile person!');
  }

}

const hobbit = new Character('Mr Baggins', 33, 'brown', 'black')
console.log(hobbit);

We could just copy paste the above and add what we need, but that's not sticking to the principal of DRY.

Instead, we can extend our Character class to create our Hobbit.

We can now add additional functionality:

class Hobbit extends Character {
  steal () {
    console.log("Let's get away!");
  }
}

const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
console.log(frodo);
frodo.smite();
frodo.steal();

And we can override previous functionality, if desired:

class Hobbit extends Character {
  steal () {
    console.log("Let's get away!");
  }
  greet (otherCharacter) {
    console.log(`Hello ${otherCharacter}.`);
  }
}

const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
frodo.greet('Bob');

We can even reference the parent class's methods and extend its original functionality:

class Hobbit extends Character {
  steal () {
    console.log("Let's get away!");
  }
  greet (otherCharacter) {
    console.log(`Hello ${otherCharacter}.`);
  }
  smite () {
    super.smite();
    this.steal();
  }
}

const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
frodo.smite();

This is most useful on the constructor:

class Hobbit extends Character {
  constructor (name, age, eyes, hair) {
    super(name, age, eyes, hair);
    this.skills = ["thievery", "speed", "willpower"];
  }
  steal () {
    console.log("Let's get away!");
  }
  greet (otherCharacter) {
    console.log(`Hello ${otherCharacter}.`);
  }
  smite () {
    super.smite();
    this.steal();
  }
}

const frodo = new Hobbit('Frodo', 30, 'brown', 'black')
console.log(frodo);

super is another special keyword/function. Try mispelling it, and you'll see it won't have the same functionality.


Additional Classes

Make a class of Elves, Dwarves, Men, or something else that resonates with your fantasy world. Be creative and share your class with the class.


Create a Factory

Sometimes we need to have a "factory" object that will generate other objects.

  • The purpose of the factory is so it can control the creation process in some way.
  • This is usually a single object that exists throughout the program that performs a set of functions, also called a singleton.

Let's start with magical tome.

class Tome {
  constructor (maker, serialNum) {
    this.maker = maker;
    this.spellType = spellType;
    this.serialNum = serialNum;
  }
  cast () {
    console.log('Casting a spell!');
  }
}

const fireTome = new Tome('Merlin', 'Fire', 1);
console.log(fireTome);

Now let's make a factory class that will make tomes for us.

class Factory {
  constructor (maker) {
    this.maker = maker;
    this.tomes = [];
  }
  generateTome (spellType) {
    const newTome = new Tome(this.maker, spellType, this.tomes.length);
    this.tomes.push(newTome);
  }
  findTome (index) {
    return this.tomes[index];
  }
}

const merlin = new Factory('Merlin');
merlin.generateTome('Fire');
merlin.generateTome('Water');
merlin.generateTome('Earth');
merlin.generateTome('Air');
console.log(merlin);
console.log(merlin.findTome(0));

Now we can easily create another new factory that produces its own tomes.

const gandalf = new Factory('Gandalf');
gandalf.generateTome('Light');
gandalf.generateTome('Dark');
console.log(gandalf);
console.log(gandalf.findTome(0));

Static Properties

Sometimes you want to define properties that pertain to the class as a whole, not the instance. This allows us to limit, somewhat, what the user of a class can do.

class Character {
  static eyeColors () {
    return ['blue', 'green', 'brown'];
  }
  // rest of class definition here...
}
// more code...
const superman = new Character('Clark Kent', 30, Person.eyeColors()[0], 'black');

Going Forward

Use the information and examples provided thus far to expand your game of Objects and Orcs!

To give you some ideas of what you could accomplish, try to:

  • Add additional classes to handle repetitive objects.
  • Add class methods to create new actions.
  • Add interaction between the objects of classes within your methods (could two Characters trade items?).

You could, for example, create an Inventories class that includes properties in the form of items and methods like add or remove. Your Characters could then have Inventory objects within their properties, and trade methods that access Inventory.add and Inventory.remove, as an example. Is this the best approach for this functionality? Explore and discover!

The possibilities are nearly endless. If you've run out of time for this lab, feel free to return later and continue your adventure!

Copyright © Per Scholas 2024