1

class B extends class A. I'll call A the parent and B the child. Both have constructors. B calls super() inside of its constructor. Both have a method with the same name. Perhaps just by coincidence or mistake both have a 'this.x' variable. There then becomes no way to access the parent's this.x variable. It then becomes a point of perhaps unintended communication between the child and parent.

   class A {
      constructor(){
        this.x = "super x!";
      }
      logx(){
        console.log(this.x);
      }
    }

    class B extends A{
      constructor(){
        super();
        this.x = "derived x.";
      }
      logx(){
        super.logx();
      }
    }

    let b = new B;
    b.logx();  // expected "super x!", but it prints "derived x".

It might be the case that class A comes from a library, or was written by someone else. It might even be the case that the author of class A comes and edits the code and adds a new variable, which then aliases against a child that he doesn't even know exists. The author of the child class must then become an avid reader of changes in the parent class so he or she can update his or her own code accordingly, if indeed this author is even still on the project. (It is such a bug that has lead me here today, this is the distillation of it.)

In the following code I prevent this problem by giving every variable a prefix that is the same as the class name. Then I get the expected behavior. Surely there is a better way. Perhaps some of these private / public keywords would help?

      constructor(){
        this.A_x = "super x!";
      }
      logx(){
        console.log(this.A_x);
      }
    }

    class B extends A{
      constructor(){
        super();
        this.B_x = "derived x.";
      }
      logx(){
        super.logx();
      }
    }

    let b = new B;
    b.logx();  // expected "super x!", and indeed it prints "super x!"

This also happens for method calls, though that is less surprising because a) that is considered 'polymorphism' b) it is usual that changes to the interface of upstream code has downstream code effects. However, a programmer might have some auxiliary functions not intended to be on the interface, and if a child class author happens to think of the same auxiliary function name, or extends the interface with a function by that name ...

   class A {
      constructor(){
        this.x = "super x!";
      }
      f(){
        console.log("I am a super f()!");
      }
      logx(){
        this.f(); // aliased - polymorphism behavior
        console.log(this.x);
      }
    }

   class B extends A{
      constructor(){
        super();
        this.x = "derived x.";
      }
      f(){
        console.log("I am a derived f()");
      }
      logx(){
        super.logx();
      }
    }

   let b = new B;
   b.logx();

console output:

I am derived f()
derived x.

As per Jonas Wilms comment on his unwinding of what is going on, it is true that the composition pattern can be used to encapsulate the parent's data and thus prevent aliasing by accident:

   class A {
      constructor(){
        this.x = "super x!";
      }
      f(){
        console.log("I am a super f()!");
      }
      logx(){
        this.f();
        console.log(this.x);
      }
    }

    class B {
      constructor(){
        this.a = new A();
        this.x = "derived x.";
      }
      f(){
        console.log("I am a derived f()");
      }
      logx(){
        this.a.logx();
      }
    }

    let b = new B;
    b.logx();

And it behaves as expected, the console output:

    I am a super f()!
    super x!

However, this is not without its problems. Firstly, the instanceof operator does not work. Secondly, we don't inherit any methods. The author of the child class will have to add stubs that just take the arguments and pass them to the parent class method. This might have performance implications. See ES6 et al. is it possible to define a catch-all method?.

.. it seems this question boils down to, 'how do you define what is on the interface, and what isn't?' and gee, there is a demonstration of why someone might like to do this.

  • based on the interactions thus far, I just updated the description to make it a bit clearer, I hope. I removed the current solution check (sorry Jonas). Though the problem is now better understood, a solution still hasn't be given. Now that the problem is better understood, perhaps the solution will be more obvious to someone. –  Apr 16 '19 at 07:41
  • 1
    I think there is no solution (except Typescript). That's just a design choice (mistake) in JS, that is so deeply in the language that you cannot change it. – Jonas Wilms Apr 19 '19 at 13:00
  • using composition instead of inheritance gives the parent its own namespace, i.e. no collisions. However, then the search up __proto__ chain will not find the composed parents methods. I'm curious to know how call is done and perhaps modifying that. I asked another question on that, https://stackoverflow.com/questions/55691740 I've also been playing with the interpreter and writing a doc today so as to raise my level of understanding with js. –  Apr 20 '19 at 13:32

1 Answers1

3

Actually your class hierarchy is equal to

 // a constructor is just a function
 function A() {
  this.x = "super x!";
}

A.prototype.logx = function() { console.log(this.x); };

function B() {
  A.call(this); // "this" gets passed, no new instance gets created
  this.x = "derived x";
}

B.prototype = Object.create(A.prototype); // extending a class basically lets the prototype of the class inherit the prototype of the superclass
B.prototype.constructor = B;

B.prototype.logx = function() {
  A.prototype.logx.call(this); // we can reference A#logx as it exists on the prototype
};

// using "new" basically creates a new object inheriting the prototype, then executes the constructor on it
let b = Object.create(B.prototype);
B.call(b);

So while there are actually two logx methods that you can reference (one on A's prototype and one on B's), there is just one instance (this) that gets passed through during construction, and setting a property of an object overrides the previous value. Therefore you are right, there is no way to have different properties with the same name.

Gosh hope one doesn't need to do something such as adopt a convention of giving every variable a prefix based on its class name if one wants to assure that parent variables remain independent

I really recommend using Typescript to keep an eye on the structure (there is a private and readonly property modifier). In JS you could use Symbols to mimic private properties:¹

 class A {
   constructor() {
     this[A.x] = "stuff";
   }
 }

 A.x = Symbol();

 class B extends A {
   constructor() {
     this[B.x] = "other stuff";
   }
 }

 B.x = Symbol();

 console.log(new B()[A.x]);

(for sure you can keep the Symbols in any kind of variable, no need to make it part of the class).

Or you just give up the inheritance thing, and compose B with A:

 class B {
   constructor() {
     this.a = new A();
     this.x = "new x";
   }
 }

 (new B).x
 (new B).a.x

Will this also happen for method calls?

Yes, as the inheritance chain for an instance of B is:

 b -> B.prototype -> A.prototype

The method will be looked up in b first, then in B and finally in A, so if there is a method with the name "logx" in both A and B, the one of B will be taken. You could also do:

 b.logx = function() { console.log("I'm first!");

so, what does one do when writing parent code, if one wants the parent f()?

You could directly call it on the prototype:

 A.prototype.logx.call(b /*...arguments*/);

from within a method you can take this instead of a concrete instance (b in this example). If you don't want to to take the specific implementation, but the one of the superclass, use super.logx() just as you did.


¹ To be honest: I never had any problems with that, as long as you name your properties properly, names will really rarely clash.

Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 2
    Nice explanation — this is a really confusing part of JS if you are coming from other OO languages. – Mark Apr 14 '19 at 17:22
  • so I guess the same will happen if I call a function, say this.f() from within a super class, it will then go and call this.f() defined in the child class? ... I'll go try this .. –  Apr 14 '19 at 17:29
  • 1
    @user244488 yes exactly. But thats actually the sense of polymorphism – Jonas Wilms Apr 14 '19 at 17:29
  • As for polymorphism, CLOS would do something similar to this. However, in C++ polymorphism goes through virtual pointer tables, I don't believe the parent call to the parent method statically compiled in, i.e. not called through a pointer, would go to the child method. Here and in CLOS, it makes it difficult to have a parent that does parent stuff as a helper to the child, as the parents function calls all loop back to the child. As a wild example, if I had a two dimensional vector the parent took care of, and the child added another dimension, and used the parent for the first two. –  Apr 14 '19 at 17:40
  • @user244488 in that case, choose composition over inheritance. I've written a library with 15k+ lines of code and haven't used classes at all (just objects and functions, however with Typescript to keep track of them), so there is no need for classes at all (if you think they are confusing, which I would partially agree to) – Jonas Wilms Apr 14 '19 at 17:42
  • Jonas that makes sense. Thank you. .. I was thinking about rearranging this as composition, there isn't too much here, I'll give that a shot. –  Apr 14 '19 at 17:48
  • 2
    @mark actually I think this is getting worse by the way the comittee is focusing on extending the class syntax (private fields, field initializers etc.). It is making things even more confusing (even for me, I grew up with ES5). (Oh and thanks :)) – Jonas Wilms Apr 14 '19 at 17:51
  • @user244488 as always, glad to help :) – Jonas Wilms Apr 14 '19 at 17:51
  • Jonas, is it possible to write a catch-all method? Sure would make composition simpler. https://stackoverflow.com/questions/55691740 –  Apr 15 '19 at 15:25
  • IMHO the approach of 'names rarely clash' is just begging for difficult to find bugs and difficult to maintain code. Typescript solves this simply with public private keywords? I was hoping to keep this in javascript but that might be a convincing reason to convert to Typescript. –  Apr 16 '19 at 07:50
  • @user244488 I personally never just extend a class, I always skim through its fields and methods to check wether it makes sense to extend it at all. And if a project gets that large that you won't remember the class you written a year ago, then I always use Typescript (because the IDE spots so much mistakes then) – Jonas Wilms Apr 16 '19 at 08:13