5.4 Writing Your Own Classes
SLO 3
Describe, design, implement, and test structured programs using currently accepted methodology.
Here you design the class instead of using one: a constructor that decides what each object needs, methods that act on `this`, and a decision rule in §5.4.6 for when a class earns its keep at all. Choosing the structure is the design verb in the outcome, not just writing it.
Learning Objectives
By the end of this section, you will be able to:
- Write your own class with a constructor and properties.
- Use
thiscorrectly inside a method. - Write methods that take parameters, return values, and call other methods on the same instance.
- Use composition to wrap a sprite inside your own class instead of extending it.
- Store many instances of a class in an array and loop over them.
- Decide when a problem calls for a class and when a procedural solution is simpler.
This section takes the win condition you hand-built in §5.2.5 — a lone goal variable, a manual distance check, a goal = null after collecting — and turns it into a proper Goal class, using the class/instance distinction §5.3 just introduced. By the end, your game will support any number of goals without a single copy-pasted distance check.
5.4.1 The new Operator and the Constructor
new ClassName(...) does three things mechanically: it allocates a new, empty object; it runs the class's constructor method with this bound to that new object; and it returns the finished object to you.
The constructor is the special method that new calls. It receives whatever arguments you passed to new, and its job is usually to store them onto this so the rest of the class can use them later.
§5.3 handed you classes somebody else wrote — Sprite, Canvas, Group. This section is where you write one, and the difference is smaller than it sounds: same new, same instances, just a blueprint whose contents you chose. Two things here cut against the grain of a chapter that has spent two sections selling classes. §5.4.4 puts a sprite inside your class rather than extending one, and this book never teaches extends at all. And §5.4.6 closes by working out when a class is not worth writing. Both are deliberate — knowing where a tool stops applying is part of knowing the tool.
The special method inside a class, named constructor, that new runs automatically when building an instance. It receives the arguments passed to new and is responsible for storing them onto this so the rest of the class can use them.
Write the start of a Goal class: a constructor that takes x and y, creates a gold, non-colliding sprite at that position (exactly like §5.2's goal setup), and stores it as this.sprite. Create one Goal instance and confirm it appears on screen.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
The gold circle appears exactly where §5.2's hand-built goal did — but this time the three setup lines live inside a constructor instead of loose in setup(). Every future Goal you create runs those same three lines automatically.
5.4.2 Properties and this
§5.3.3 defined a property as a piece of data belonging to one instance — .color, .bounciness, .collected. Now you write the code that creates one: inside a constructor, this.name = value gives the instance you're building its own copy of that property. Two instances of the same class hold their properties independently — writing to one never touches the other. Inside a method, this always refers to one specific thing: the instance the method was called on.
Assigning this.name = value inside a constructor (or a method) is how a class gives each of its instances a property. Unlike .bounciness's engine-level default (§5.3.3), a property your own class assigns has no fallback — if the constructor doesn't set it, the instance simply doesn't have it.
Calling goalA.isNear(player) (§5.4.3 writes this method next), rewrite the method body in your head as if it said goalA.sprite instead of this.sprite. Calling goalB.isNear(player), rewrite it as goalB.sprite. Same method code, different substitution, depending on which instance made the call.
Inside a class's constructor or a method, this refers to the specific instance the code is currently running on. A useful trick for reading a method: mentally replace every this with whatever variable sits on the left of the dot at the call site.
Add a this.collected = false property to Goal's constructor, alongside this.sprite. Create two goals and confirm each has its own independent .collected value — setting one to true should not affect the other.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Setting goalA.collected = true has no effect on goalB.collected, which is still its own default false — each Goal instance owns a completely separate copy of every property the constructor set up.
5.4.3 Writing Methods
Inside a class body, methods are written without the function keyword. A method can take no parameters, take parameters, return a value, or call another method on the same instance via this.otherMethod().
A function written inside a class body, without the function keyword, that operates on this — the instance it was called through. A method can take parameters, return a value, and call other methods on the same instance.
Give Goal an isNear(player) method that runs the exact distance check §5.2.5 wrote by hand, but returns true/false instead of directly deleting anything. Then, in draw, check goal.isNear(player) and log "Close!" when it's true.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
isNear takes player as a parameter and reads this.sprite to get its own position — the same arithmetic §5.2.5 wrote inline, now packaged as a question you can ask any Goal instance. It logs every frame you're close, same repeat-every-frame behavior §6.2 will name properly as an overlap.
5.4.4 Composition: Wrapping a Sprite
Goal already does this without it being named yet: this.sprite = new Sprite(...) stores a sprite as a property, rather than making Goal extends Sprite. This idiom is called composition.
Composition keeps the boundary explicit. Goal's methods read and write this.sprite.color, this.sprite.x, and so on, so the moSHion API surface you're relying on is always visible in your own code, instead of being inherited silently from a parent class you haven't fully learned yet.
A design idiom where a class stores another object as a property (this.sprite = new Sprite(...)) rather than inheriting from it (extends). The class's own methods explicitly read and write through that stored property.
Finish Goal: add a collect() method that deletes this.sprite (see §5.2.5's delete()) and sets this.collected = true. In draw, call collect() when isNear(player) is true and the goal hasn't been collected yet, then log "You win!" exactly once.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Compare this to §5.2.5's version: there, "have I already won" was tracked by setting the goal variable to null and wrapping the whole check in if (goal). Here, the same idea — "don't fire twice" — lives inside the object itself as this.collected, checked with !goal.collected before asking isNear. The object owns its own state instead of the surrounding code tracking it from outside.
5.4.5 Arrays of Objects
A single Goal works fine when there's exactly one. As soon as there are several, store them in an array and loop over it — exactly like you already loop over sprites and groups.
Spawn three Goal instances into an array instead of one bare goal variable. In draw, loop over the array and call collect() on any uncollected goal the player is near. Log "All goals collected!" the first frame every goal in the array has .collected === true.
▶ Press Run to see the output…
Solution
▶ Press Run to see the output…
Each Goal in the array is its own instance with its own sprite, its own .collected flag, and its own isNear/collect behavior — the loop treats them identically without caring how many there are. goals.every(...) reads naturally once you have an array: "is every one of these collected?"
5.4.6 Procedural vs. Object-Oriented: A Decision Rule
Line up §5.2.5's original win condition against §5.4.5's Goal array and the difference is the whole lesson:
Procedural (§5.2.5) — one goal variable, one inline distance formula, "already won" tracked by nulling the variable:
if (goal) {
const dx = player.x - goal.x;
const dy = player.y - goal.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 25) {
console.log('You win!');
goal.delete();
goal = null;
}
}
Object-oriented (§5.4.5) — an array of Goal instances, each bundling its own sprite, distance check, and collected flag:
for (const g of goals) {
if (!g.collected && g.isNear(player)) {
g.collect();
}
}
The procedural version was the right call when there was exactly one goal — a class would have been ceremony for nothing. It stopped being the right call the moment a second goal showed up, because every additional goal means another copy-pasted if (goal2) { ... } block, another variable to null out, another chance to forget one.
A simple three-question checklist decides which approach fits a given problem: (1) Is there more than one of this thing? (2) Does each one carry its own state? (3) Does each one have its own behavior tied to that state? Three yeses point toward a class — which is exactly what happened to goal between §5.2 and §5.4.
Two vocabulary words worth knowing, even though this book doesn't build with them: inheritance lets one class extend another (class PowerUp extends Collectible), and polymorphism lets different classes respond to the same method name in their own way. Both matter in larger programs; neither is needed for anything in this book, and composition (§5.4.4) covers the same ground for moSHion's own classes.
Chapter 6 will introduce enemies. Sketch (in comments, no need to run it) what an Enemy class's constructor and a damage(n) method would look like, following the same shape as Goal's constructor and collect(). What property would damage need that Goal didn't?
Solution
class Enemy {
constructor(x, y, hp) {
this.sprite = new Sprite(x, y, 30, 30);
this.sprite.color = 'crimson';
this.hp = hp; // Goal didn't need a health value — Enemy does
}
damage(n) {
this.hp -= n;
if (this.hp <= 0) {
this.sprite.delete();
}
}
}
The shape is identical to Goal: a constructor that builds a composed sprite plus whatever game-specific data the object needs, and a method that changes that data and reacts once a condition is met. Goal needed collected; Enemy needs hp. The pattern — not the specific properties — is what transfers.
5.4.7 Challenge: Extend It Yourself
No starter code this time — you build it. Take the Goal class from this section and give it a second collectible type, Hazard, that follows the exact same shape (constructor composing a sprite, an isNear method, a method that reacts to contact) but instead of collecting points on contact, it resets the player back to its starting position. Spawn a mix of Goal and Hazard instances in one array and loop over both.
Hint (try the Challenge yourself first!)
Goal and Hazard don't need to share a parent class for this — composition means each one independently wraps its own sprite. Give both a .sprite and an isNear(player) method with the same signature, and a single loop can check isNear on every object in the mixed array without caring which class each one is.
Problem Set 5.4
Problem 1. Why did turning goal into a Goal class matter once a second goal showed up, when it didn't matter for just one?
Solution
Step 1 — Recall the one-goal case: With exactly one goal, the procedural version (§5.2.5) was fine: a single goal variable, one inline distance check, and goal = null to mark "already won." Adding a class would have been ceremony with no benefit.
Step 2 — See what breaks with two goals: A second goal means a second variable (goal2), another copy-pasted if (goal2) { ... } block with its own distance formula, another null-out line — and another chance to forget one of them.
Step 3 — See what the class buys you: With a Goal class, each goal bundles its own sprite, its own .collected flag, and its own isNear/collect behavior. Goals go in an array, and one loop handles any number of them:
for (const g of goals) {
if (!g.collected && g.isNear(player)) {
g.collect();
}
}
Adding a third or twentieth goal costs zero new logic — just another entry in the array.
Answer: The class mattered once there were multiple goals because it eliminated per-goal copy-pasted code: state (.collected) and behavior (isNear, collect) live inside each instance, so an array plus one loop scales to any number of goals, while the procedural version needed a new variable and a new check block for every single goal added.
Problem 2. What three things does JavaScript do when you use new to create an instance from a class?
Solution
Step 1 — Allocation: new allocates a new, empty object in memory. This is the blank instance that will become your object.
Step 2 — Run the constructor: It runs the class's constructor method with this bound to that newly allocated empty object. Any arguments you passed to new are handed to the constructor, which typically stores them onto this.
Step 3 — Return the finished object: It returns the finished object to you, so you can store it in a variable (e.g., goal = new Goal(360, 340)).
Answer: JavaScript (1) allocates a new empty object, (2) runs the class's constructor method with this bound to that new object (passing along the arguments given to new), and (3) returns the finished object to the caller.
Problem 3. What is a constructor, and what happens if a class doesn't define one?
Solution
Step 1 — Define the constructor: The constructor is the special method inside a class, named constructor, that new runs automatically when building an instance. It receives whatever arguments were passed to new, and its job is usually to store them onto this so the rest of the class can use them later.
Step 2 — What if it's missing? If a class doesn't define a constructor, JavaScript supplies an empty default one automatically — new ClassName() still works; it just builds the instance without running any of your setup code, so no properties get set.
Answer: A constructor is the special method named constructor that new calls automatically, receiving the new arguments and storing them onto this. If a class doesn't define one, JavaScript uses an implicit empty constructor — instances are still created, but no custom setup happens.
Problem 4. Inside a method, what does this refer to? Give the substitution trick for reading goalA.isNear(player) versus goalB.isNear(player) from the same class.
Solution
Step 1 — Define this: Inside a method, this refers to the specific instance the method was called on — whatever variable sits on the left of the dot at the call site.
Step 2 — Apply the substitution trick: Mentally rewrite the method body replacing every this with the calling instance:
- Calling
goalA.isNear(player)→ read the body as if everythis.spritesaidgoalA.sprite. - Calling
goalB.isNear(player)→ read the same body as if everythis.spritesaidgoalB.sprite.
Same method code, different substitution — which is why the two calls can give different answers even though they run identical lines.
Answer: this refers to the instance the method was called through. For goalA.isNear(player), mentally replace each this with goalA; for goalB.isNear(player), replace each this with goalB — same code, different instance substituted in.
Problem 5. Write a Counter class with a tick() method that increments this.n and a reset() method that sets this.n back to 0.
Solution
Step 1 — Write the class skeleton: A Counter needs one property, n, initialized in the constructor, plus two methods written without the function keyword.
class Counter {
constructor() {
this.n = 0;
}
tick() {
this.n += 1;
}
reset() {
this.n = 0;
}
}
Step 2 — Check the behavior: Each instance owns its own independent n:
const c = new Counter(); c.tick(); c.tick(); console.log(c.n); // 2 c.reset(); console.log(c.n); // 0
tick() increments this.n by 1 each call; reset() assigns 0 back onto this.n.
Answer:
class Counter {
constructor() {
this.n = 0;
}
tick() {
this.n += 1;
}
reset() {
this.n = 0;
}
}
Problem 6. What is composition, and why does Goal use it instead of extends Sprite?
Solution
Step 1 — Define composition: Composition is the design idiom where a class stores another object as a property — e.g., this.sprite = new Sprite(...) — rather than inheriting from it with extends. The class's own methods then explicitly read and write through that stored property (this.sprite.color, this.sprite.x, …).
Step 2 — Why Goal chooses it: Composition keeps the boundary explicit. Every engine feature Goal relies on appears visibly in Goal's own code as this.sprite.something, instead of being silently inherited from a parent class you haven't fully learned yet. You only depend on the API surface you actually wrote down.
Answer: Composition means wrapping another object as a property (this.sprite = new Sprite(...)) rather than extending its class. Goal uses it because it makes the sprite API it depends on explicit and visible in its own methods, avoiding silent inheritance from a parent class the student hasn't learned yet.
Problem 7. You have 20 collectible items in a game, each with its own position and point value. Would you use one array of parallel values or one array of class instances? Justify your answer using the three-question checklist from §5.4.6.
Solution
Step 1 — Run the checklist: Apply the three questions from §5.4.6 to the collectible items:
- Is there more than one of this thing? Yes — 20 items.
- Does each one carry its own state? Yes — each has its own position and its own point value.
- Does each one have its own behavior tied to that state? Yes — each is checked for proximity and collected independently, using its own position and awarding its own points.
Step 2 — Compare the alternatives: One array of parallel values (e.g., separate arrays for x-positions, y-positions, point values) forces you to keep three arrays index-aligned by hand — add or remove an item and you must update all three in sync, a classic source of bugs. An array of class instances keeps each item's data bundled together, so one loop over the array handles all 20 identically.
Answer: Use one array of class instances. All three checklist questions answer yes — many items, each with its own state (position + point value), each with behavior tied to that state — which points toward a class. Parallel arrays would require manually keeping multiple arrays index-aligned, while instances bundle each item's data so a single loop manages all 20 safely.
Problem 8. What is the difference between inheritance and composition? (You only need to define inheritance — this book doesn't use it.)
Solution
Step 1 — Define inheritance: Inheritance lets one class extend another — class B extends A — so B automatically gets all of A's properties and methods, and can add or override some of its own. (This book's shPlay classes don't use it.)
Step 2 — Contrast with composition: Composition stores another object as a property (this.sprite = new Sprite(...)) and accesses its features explicitly through that property. Nothing is inherited silently — the wrapper class's methods spell out every this.sprite.something they rely on.
Step 3 — State the key difference: Inheritance creates an "is-a" relationship (a PowerUp is a Collectible) with features flowing in implicitly from the parent; composition creates a "has-a" relationship (a Goal has a sprite) with features accessed explicitly through the stored object.
Answer: Inheritance is one class extending another (class B extends A), where B silently receives A's properties and methods. Composition instead stores another object as a property and reads/writes it explicitly through that property. Inheritance is implicit "is-a"; composition is explicit "has-a."
Key Terms
| Term | Definition |
|---|---|
| Constructor | The special method, named constructor, that new runs to build an instance from its arguments |
| Setting a property | Assigning this.name = value inside a constructor or method, giving one instance its own copy of a property (see §5.3.3 for what a property is) |
| this | Inside a method, refers to the specific instance the method was called on |
| Method | A function written inside a class body that operates on this |
| Composition | Storing another object as a property (this.sprite = new Sprite(...)) instead of inheriting from it |
| Inheritance | One class extending another (class B extends A); not used by this book's moSHion classes |