Usually we trend to think in terms of classes & instances.
Let's do a quick review to prototype
and class
theory.
We use regular functions to create the illusion of classes
function foo() {
console.log('this is just a regular function');
this.foo = 'foo';
} // « undefined
const f = new foo(); // « Object { foo: 'foo' }
// this is just a regular function
f.foo; // « 'foo'
Then, the way of creating class methods is adding a property to foo's prototype
foo.prototype.sayHi = () => {
console.log('hi');
return 1;
}; // « function sayHi()
f; // « Object { foo: 'foo' }
f.sayHi(); // « 1
// hi
This is so standard that ES5 (and ts
before them) introduced syntactic sugar:
class goo {
constructor() {
this.goo = 'goo';
}
sayHi() {
console.log('hi');
return 1;
}
} // « undefined
g = new goo(); // « Object { goo: "goo" }
g.sayHi(); // « 1
// hi
The truth is...
There are no classes. Neither instances. While in class-oriented languages, multiple copies (instances) of a class can be made, like stamping or copying behavior out from a mold. In js
we have just regular friendly objects.
...js
is prototype-oriented.
What we're truly doing on the examples above is creating a new object and linking them to another plain, regular object.
This linked object is the prototype
.
We are linking each new object to the prototype
of the function we've called.
There are plain js-objects in memory; no more, no less.
function foo() {
this.foo = 'foo';
}
foo.prototype.sayHi = () => console.log('say hi');
f = new foo();
/*
------------- linked to ----------------- linked to ------------------ linked to
| foo: string | ---------> | sayHi: Function | ---------> | { toString... } | ---------> null
------------- ----------------- ------------------
^ ^ ^
f foo.prototype Object.prototype
*/
foo.prototype; // Object { sayHi: Function }
Object.getPrototypeOf(f) == foo.prototype; // « true
Object.prototype; // « Object { ... }
Object.getPrototypeOf(foo.prototype) == Object.prototype; // « true
Object.getPrototypeOf(Object.prototype); // « null
Thing is, IMO this is even more powerful (and elegant) than classic instances. Since everything are in-memory objects, we could even modify the thing in execution time:
function player() {}
player.prototype.sayHi = () => console.log('hi');
a = new player();
b = new player();
a.sayHi(); // « 'hi'
b.sayHi(); // « 'hi'
// we can modify the behaviour in execution time
player.prototype.sayHi = () => console.log('hello');
a.sayHi(); // « 'hello'
b.sayHi(); // « 'hello'
Applying the theory
With this in mind. We (our team) challenge to stop thinking in terms of classes & instances, and start thinking on prototype objects.
Imagine you need two objects, o1
and o2
, each one overriding some default behavior/values. This is how we'd code that:
const defaultBehaviour = {
name: 'guest',
sayHi() {
return 'hi ' + this.name;
},
foo() {
return 42;
},
};
const o1 = Object.setPrototypeOf({ name: 'Jane Doe' }, defaultBehaviour);
o1.sayHi(); // « hi Jane Doe
o1.foo(); // « 42
const o2 = Object.setPrototypeOf({ foo: () => 100 }, defaultBehaviour);
o2.sayHi(); // « hi guest
o2.foo(); // « 100
While the class sugar-syntax is perfectly valid. Let's remove the mask and claim the true behavior of js
🦾
--
I can't recommend hard enough to read Kyle Simpson.
--
Cover image from undraw.co
Thanks for reading 💚.
Top comments (0)