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)