Why is Object.setPrototypeOf discouraged / ineffective when building a Javascript class hierarchy?

I read an article on storing data in private class hierarchies here . The way I do it is different. I am using factory functions with Object.setPrototypeOf (obj, prototype) .

Why isn't my way of doing this considered good practice?


Here's my way of doing it:

I don't need public variables, so I create my dog ​​object using 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 food:

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

it is supposed to refer to the number of times myDog ate.


ps 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 creating public variables on my object. If I try to do methods like these it looks ugly (I think the underscore syntax is ok, but not very proprietary).


Decision:

If we create 10 dogs with the same animal as their prototype, "let numTimesEat" is shared. If the dog eats once, you don't want numTimesEat to be 10.

So besides setting up the prototype 10 times (slow operation is done many times), you also need to create 10 animals for these 10 dogs in order to delegate them.


Update: you can instead 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 what Javascript is trying to do OOP .

+3


source to share


3 answers


You create two instances and then set one of the prototypes of the other:

var myAnimal=makeAnimal(),myDog=makeDog();
Object.setPrototypeOf(myDog, myAnimal);

      

So instead of this simple inheritance, we basically want:

myDog -> Dog.prototype -> Animal.prototype
myDog2 ->
myDog3 ->

                   Animal ->
                  Animal2 ->

      



You do it:

myDog -> myAnimal
myDog1 -> myAnimal1
myDog2 -> myAnimal2
Animal
Animal2

      

So instead of two prototypes containing all the functions and lightweight instances just storing data, you have 2n (one animal for each dog) instances containing related function references and data. it is really inefficient when constructing many elements, and the assignment of functions in a factory is also not the case, so can stick to class inheritance since it solves both problems. Or, if you want to use setPrototype, use it once (then its slowness has little effect):

var Animalproto = {
 birthday(){...}
}

var Dogproto={
 bark(){ ... }
}

Object.setPrototypeOf(Dogproto,Animalproto);

function makeAnimal(){
  return Object.create(Animalproto);
}

function makeDog(){
  return Object.create(Dogproto);
}

      

+1


source


Simple.

If we didn't have prototypes or this

in Javascript, we could still do OOP and inheritance as you described:

  • each object contains its own methods
  • methods share state in the closure

or even no hidden state:

  • each object contains its own methods and state
  • methods have a reference to the object in the closure

Now you can say, "Hey, my object has 2 state variables and 10 methods, isn't it wasteful that I have to make 10 copies of the methods whenever I need an instance of a new object?"



Then we could go like this: "yes, let's actually share functions between all objects of the same type." But this creates two problems:

  • where do we put them? (okay, let's say one object is called "prototype", let's each instance have a hidden reference to that prototype)
  • How does a method know which object it is called on? We can no longer have it in the closure ... (pass this

    as a hidden parameter for each method)

... and thus we arrive at the current state of standard JS OOP. Somewhere along the way, we sacrificed the ability to keep private state in method closures (since there is only 1 method on all objects).


So, you want to actually NOT go there and stay on a square where each object has its own copy of its methods. This is good and you have a personal state! But why do you need prototypes at the moment? They cannot serve their original purpose. One object is bark()

completely unrelated to another bark()

, they have different closures, they cannot be used together. The same goes for any inherited methods.

In this situation, since you are already duplicating all the methods, you can simply store them in the object itself. Adding a (separate) prototype to each object and placing the superclass methods there doesn't get you anything, it just adds another layer of indirection to method calls.

+3


source


I think that regular inheritance is faster than switching prototypes and also the way it is maintained (setPrototypeOf is an es6 function). Also you can mess things up if you use it (consider creating an html element and switch its prototype to something else).

In your case, there is a better way you can use setPrototypeOf instead. Instead, in your makeDog and makeAnimal factory functions, you can actually add properties to an existing object instead of creating a new object. You can do it with Object.assign (which is polyfillable) like this:

const makeDog = (name, animal = {}) => {
    return  Object.assign(animal, { bark: () => { console.log(name) } })
}
const myAnimal = makeAnimal()
const myDog = makeDog("sniffles", myAnimal)

      

If you want to be efficient (use regular inheritance) and still keep private variables, you can still do that! I wrote a blog post about this, and here's a gist that you can use to make it easier to create private members. Finally, here's a good article on whether OOP structures as inheritance are good or bad.

+1


source







All Articles