Object-oriented
JavaScript Object Creation Patterns
1. Factory Functions
A factory function is a function that creates and returns an object.
function createPlayer(name) {
return {
name,
health: 100,
attack() {
return `${this.name} attacks!`
}
}
}
const player = createPlayer("Hero")
- Notes
- No
newkeyword required. - Great for composition and functional-style code.
- Methods using
thiswork normally:- Problems occur when methods are detached from the object:
- No
const player = createPlayer("Hero")
player.attack()
// Hero attack
const attack = player.attack
attack() // 'this' is lost
// undefinied attacks
The This Issue
- Why is
thisundefined in detached modethisvalue is decided at call time, based on the left side of the dot.- attached mode
this = player: Because the call happens with a base object (before the dot).
- detached mode
this = undefined, because you're just calling a plain function, not a method. no object before the call
player.attack() // Hero attack
const attack = player.attack
attack() // undefinied attacks
- Ways to fix the
thisissue- use
.bind()- what is
bind: Returns a new function with a permanently bound thisvalue(and optionally preset arguments), without invoking it immediately.
- what is
- use
.callor.apply- what is
call: invokes a function with a specifiedthisvalue and arguments passed individually. - what is
apply: invokes a function with a specifiedthisvalue and arguments passed as an array.
- what is
- wrap in arrow function
- Use Closure based objects to avoid
thisissue.
- use
const attack = player.attack.bind(player);
attack(); // Hero attacks!
const attack = () => player.attack();
attack(); // Hero attacks!
const attack = player.attack;
attack.call(player);
attack.apply(player);
Tradeoffs
- Every created object gets its own copy of methods.
const p1 = createPlayer("A")
const p2 = createPlayer("B")
p1.attack === p2.attack // false
This can become a memory concern when creating many objects.
2. Factory Functions with Closures
Closures allow truly private state and avoid relying on this.
function createCounter() {
let count = 0
return {
increment() {
count++
},
getCount() {
return count
}
}
}
const counter = createCounter()
counter.increment()
console.log(counter.getCount())
Benefits
- Private state.
- Encapsulation.
- No
thisissues.
Tradeoffs
- Methods are recreated for every object.
3. ES6 Classes
Classes are syntactic sugar over constructor functions and prototypes.
class Enemy {
constructor(name) {
this.name = name
this.health = 100
}
attack() {
return `${this.name} attacks!`
}
}
const enemy = new Enemy("Goblin")
Benefits
JavaScript uses prototypal inheritance.
This:
class Enemy {
attack() {}
}
is roughly equivalent to:
function Enemy() {}
Enemy.prototype.attack = function () {}
Class methods are stored on the prototype and shared by all instances.
const e1 = new Enemy()
const e2 = new Enemy()
e1.attack === e2.attack // true
When to Use Which
Use Classes When
- You need
instanceof. - You want inheritance hierarchies.
- Your team prefers OOP.
- You create many objects and want shared methods.
player instanceof Player
Use Factory Functions When
- You prefer composition over inheritance.
- You need private state.
- You want a functional programming style.
- Object relationships are flexible.