I read an article about keeping data in class hierarchies private here. The way I do it is different. I use factory functions with Object.setPrototypeOf(obj, prototype).
Why is my way of doing it not considered good practice?
Here's my way of doing it:
I don't want public variables, so I create my dog object with a factory function:
const makeDog = (name) => {
return { bark: () => { console.log(name) } }
}
const myDog = makeDog("sniffles")
myDog.bark() // "sniffles"
All animals can eat and I want my dog to inherit from Animal:
const makeAnimal = () => {
let numTimesEat = 0
return {
eat: (food) => {
numTimesEat += 1
console.log( "I eat " + food.toString() )
},
get numTimesEat() { return numTimesEat }
}
}
const myAnimal = makeAnimal()
myDog will delegate to myAnimal for eating:
Object.setPrototypeOf(myDog, myAnimal)
And now I can do:
myDog.eat("shoe") // "I eat shoe"
console.log( myDog.numTimesEat ) // 1
myDog.bark() // "sniffles"
Note that myDog.numTimesEat is supposed to refer to the number of times myDog ate.
p.s. I know you can do it with classes:
class Animal {
constructor() {
this.numTimesEat = 0;
}
eat(food) {
this.numTimesEat += 1;
console.log( "I eat " + food.toString() );
}
}
class Dog extends Animal {
constructor(myName) {
super();
this.name = myName;
}
bark() {
console.log( this.name );
}
}
const dog2 = new Dog("sniffles");
dog2.eat("shoe"); // "I eat shoe"
console.log( dog2.numTimesEat ); // 1
console.log( dog2.name ); // "sniffles"
dog2.bark(); // "sniffles"
But the class keyword seems to end up producing public variables on my object. If I try to do techniques like these it kind of looks ugly (I guess the underscore syntax is okay looking but it's not really private).
Solution:
If we create 10 dogs all with the same Animal as its prototype, the "let numTimesEat" is shared. If a dog eats one time you don't want numTimesEat to be 10.
So in addition to setting the prototype 10 times (a slow operation performed repeatedly), you also have to create 10 animals for those 10 dogs to delegate to.
Update: Instead you can put everything on the newly created object
const Dog = function(name) {
let that = Animal()
that.bark = () => { console.log(name) }
return that
}
const Animal = function() {
let numTimesEat = 0
return {
eat: (food) => {
numTimesEat += 1
console.log( "I eat " + food.toString() )
},
get numTimesEat() { return numTimesEat }
}
}
const lab = new Dog("sniffles")
lab.bark() // sniffles
lab.eat("food") // I eat food
lab.numTimesEat // 1
This is MUCH cleaner than trying to do OOP in Javascript.
Simple.
If we didn't have prototypes or this in Javascript, we could still happily do OOP and inheritance, just like you described:
or even without hidden state:
Now one might say 'hey, my object has 2 state variables and 10 methods, isn't it wasteful that I need to make 10 copies of methods whenever I want a new object instance?'
Then we could go like "yeah, let's actually share the functions between all objects of the same type". But that makes two problems:
this as a hidden parameter to every method)...and thus we arrive at the current state of standard JS OOP. Somewhere along the way, we sacrificed the possibility of keeping private state in method closures (since there's only 1 method per all objects).
So you want to actually NOT go there, and stay at square one where each object has its own copy of its methods. That's fine, and you get to have private state! But why would you even need prototypes at this point? They cannot serve their original purpose. One object's bark() is completely unrelated to another bark(), they have different closures, they cannot be shared. Same for any inherited methods.
In this situation, since you're already duplicating all the methods, you can just as well keep them in the object itself. Adding a (distinct) prototype to each object and placing superclass methods there doesn't give you anything, it just adds one more layer of indirection to method calls.
Youre creating two instances, and then set the one to be the prototype of the other:
var myAnimal=makeAnimal(),myDog=makeDog();
Object.setPrototypeOf(myDog, myAnimal);
So instead of this easy inheritance we basically want:
myDog -> Dog.prototype -> Animal.prototype
myDog2 ->
myDog3 ->
Animal ->
Animal2 ->
Youre doing this:
myDog -> myAnimal
myDog1 -> myAnimal1
myDog2 -> myAnimal2
Animal
Animal2
So instead of two prototypes holding all the functions and lightweight instances just holding the data, you have 2n (one animal for each dog) instances holding bound function references and the data. thats really not efficient when constructing many elems, and assigning functions in a factory isnt either, so may stick to the class inheritance as it resolves both problems. Or if you want to use setPrototype use it once ( then its slowlyness hasnt a big effect):
var Animalproto = {
birthday(){...}
}
var Dogproto={
bark(){ ... }
}
Object.setPrototypeOf(Dogproto,Animalproto);
function makeAnimal(){
return Object.create(Animalproto);
}
function makeDog(){
return Object.create(Dogproto);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With