You might not need JavaScript classes
@soyguijarro
👨💻 Web engineer from Madrid
💼 Dev tools at Undefined Labs
🤩 JavaScript, React, Web APIs
🗣️ International tech speaker
🎓 Adalab bootcamp volunteer
💖 Community and inclusion
Ramón
Guijarro
@soyguijarro
Why we got classes
Issues and pitfalls
Alternative patterns
Recent case studies
Why we
got classes
ECMAScript 2015
Object oriented programming
Prototype vs Class
class Counter {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
}
inc() {
this.value++;
}
reset() {
this.value = this.initValue;
}
}
function Counter(initValue) {
this.initValue = initValue;
this.value = initValue;
}
Counter.prototype.inc = function() {
this.value++;
};
Counter.prototype.reset = function() {
this.value = this.initValue;
};
class StepCounter extends Counter {
constructor(initValue, step) {
super(initValue);
this.step = step;
}
inc() {
this.value += this.step;
}
}
function StepCounter(initValue, step) {
Counter.call(this, initValue);
this.step = step;
}
StepCounter.prototype =
Object.create(Counter.prototype);
StepCounter.prototype.inc = function() {
this.value += this.step;
};
StepCounter.prototype.constructor =
StepCounter;
Improvement over prototype
Familiar for new people
Issues and
pitfalls
False expectations
class Counter {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
}
inc() {
this.value++;
}
reset() {
this.value = this.initValue;
}
}
const counter = new Counter(0);
counter.inc();
console.log(counter.value); // 1
Counter.prototype.inc = function() {
this.value--;
};
counter.inc();
console.log(counter.value); // 0
Class defines an instantiable type
Prototype is a living instance
living
const counter = new Counter(0);
counter.inc();
console.log(counter.value); // 1
Counter.prototype.inc = function() {
this.value--;
};
counter.inc();
console.log(counter.value); // 0
Lack of features
Context binding
class Button {
constructor(clickHandler) {
this.click = clickHandler;
}
}
const counter = new Counter(0);
const button = new Button(counter.inc);
button.click();
console.log(counter.value); // 0 (not 1)
console.log(button.value); // NaN (not undefined)
const button = new Button(counter.inc.bind(counter));
const button = new Button(() => {
counter.inc()
});
class Counter {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
this.inc = this.inc.bind(this);
}
inc() {
this.value++;
}
reset() {
this.value = this.initValue;
}
}
class Counter {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
}
inc = () => {
this.value++;
}
reset() {
this.value = this.initValue;
}
}
class Counter {
constructor(initValue) {
this.initValue = initValue;
this.value = initValue;
}
inc = () => {
this.value++;
}
reset() {
this.value = this.initValue;
}
}
console.log(Counter.prototype);
Harder to optimize
Alternative
patterns
Plain functions
Closures
function createCounter(initValue) {
let value = initValue;
return {
inc() {
value++;
},
reset() {
value = initValue;
},
getValue() {
return value;
}
};
}
function createButton(click) {
return {
click
};
}
const counter = createCounter();
const button = createButton(counter.inc);
button.click();
console.log(counter.getValue()); // 1
console.log(button.value); // undefined
class MovieApi {
constructor(token) {
this.token = token;
}
addFavorite(user, movie) {
fetch(`/users/${user}/favorites/${movie}`, {
method: 'POST',
headers: { 'X-Auth-Token': this.token },
});
}
}
const api = new MovieApi('abc123');
api.addFavorite('johndoe', '7321');
function createMovieApi(token) {
return {
addFavorite(user, movie) {
fetch(`/users/${user}/favorites/${movie}`, {
method: 'POST',
headers: { 'X-Auth-Token': token },
});
}
};
}
const api = createMovieApi('abc123');
api.addFavorite('johndoe', '7321');
Object composition
function createBaseCounter(initValue) {
let value = initValue;
return {
reset() {
value = initValue;
},
setValue(newValue) {
value = newValue;
},
getValue() {
return value;
}
};
}
function createStepCounter(initValue, step) {
const {
setValue, ...rest
} = createBaseCounter(initValue);
return {
...rest,
inc() {
setValue(rest.getValue() + step);
}
};
}
function createCounter(initValue) {
const {
setValue, ...rest
} = createBaseCounter(initValue);
return {
...rest,
inc() {
setValue(rest.getValue() + 1)
}
};
}
function createCounter(initValue) {
return createStepCounter(initValue, 1);
}
Function composition
function withLogging(method) {
return function(...args) {
console.log(`Called with ${args.join(', ')}`)
method(...args)
}
}
const myDecoratedFn = withLogging(myFn)
Modules
let instance;
function createSingletonCounter(initValue) {
if (!instance) {
instance = createCounter(initValue);
}
return instance;
}
export default createSingletonCounter;
Recent
case studies
React hooks
function Counter({ initValue }) {
const [counter, setCounter] = React.useState(initValue);
function inc() {
setCounter(counter + 1);
}
return (
<div>
<h1>{counter}</h1>
<button onClick={inc}>Increment</button>
</div>
);
}
Vue Composition API
<template>
<div>
<h1>{state.counter}</h1>
<button @click="inc">Increment</button>
</div>
</template>
<script>
const Counter = {
setup({ initValue }) {
const state = Vue.reactive({ counter: initValue });
const inc = () => {
state.count++
};
return { state, increment };
}
}
</script>
Better logic organization
Less convoluted patterns
More type friendly
Important
takeaways
Adjust your expectations
Watch out for context issues
Consider using functions
@soyguijarro
Thank you!
You might not need JavaScript classes
By Ramón Guijarro
You might not need JavaScript classes
- 1,894