4

Javascript ES6 ( node 8.4.0 and latest chrome and recent Firefox )

I expected

class Parent {
    init(){
        console.log("Parent init") ;
        this._surname = "McClass" ;
    }
    constructor() {
        console.log("Parent constructor") ;
        this.init();
    }

    get surname(){
        return this._surname ;
    }
}

class Child extends Parent {

    init(){
        console.log("Child init") ;
    }
    constructor() {
        super();
        console.log("Child constructor") ;
        this.init();
    }    
}

var child = new Child() ;

console.log(child.surname);

to give the following output;

Parent constructor
Parent init
Child constructor
Child init
McClass

(which is what comparable C++ code gives)

Alas, I got this ;

Parent constructor
Child init
Child constructor
Child init
undefined

Am I doing something wrong or is this the correct intended behaviour and if so how is it justified ?

EDIT;

See MinusFour's answer below on how to achieve what I was trying to do / expecting.

As to why the observed output is the "correct" behaviour and justified ;

As Bergi pointed out (in comments) all calls to object methods in js are effectively "virtual" (the last method of that name added to the object's prototype inheritance chain being the first found and hence executed). It turns out calls are still effectively virtual in a class construction context.

C++ does not apply virtual method behaviour during construction but then again Java does and you get the same output (as above) in comparable Java code so there is a precedent for the observed behaviour.

Bob
  • 1,589
  • 17
  • 25
  • I wouldn't override `init` in child, just call it something else. – MinusFour Oct 04 '17 at 17:00
  • @smnbbrv , can you elaborate or give a link to the source which explains why it is correct or defines this behaviour to be correct ? – Bob Oct 04 '17 at 17:01
  • @MinusFour, but what happens if I subclass from a 3rd party class unaware that I am using a name used in the parent class - I then get unexpected behaviour I might not even be aware is happening. – Bob Oct 04 '17 at 17:07
  • Why are you using a getter that does nothing? If you just want to have a dot accessible property then add it. If you change your mind later, you can refactor to a getter without changing the calling code. – Jared Smith Oct 04 '17 at 17:10
  • @Bob, I think subclassing other classes merits checking out what they are and what they do. As far as silent unexpected behavior, that is true and unfortunate. – MinusFour Oct 04 '17 at 17:15
  • @MinusFour, I'm extending EventEmitter2 in one case for example and the whole point of using it and modules/classes like it is so I don't have to get involved with the internals. If this is how it works ( and I've just checked and typescript code does the same thing ) then classes as they are in ES6 are inherently unsafe and effectively unusable in a lot of situations. – Bob Oct 04 '17 at 17:29
  • @Jared, the getter isn't relevant to the issue in question and is a remnant/ out of context. Sorry if it has offended you :) – Bob Oct 04 '17 at 17:31
  • @Bob offended? Sorry if I gave that impression. I'm just pointing out that JavaScript getters are not like Java/C++ getters: they are invoked by dot access, not method call (meaning your getter as written is superfluous). Typically they are used for things like dynamically calculated properties. – Jared Smith Oct 04 '17 at 17:38
  • @Bob, this is pretty much how it has always been with JS even before classes. It might very well be that JS inheritance model (prototypey chain) is ill suited for class semantics. – MinusFour Oct 04 '17 at 17:46
  • 2
    There is no concept of "private" properties/methods in JavaScript. Everything is public. Don't you have be aware of public methods when extending classes in other languages? – Felix Kling Oct 04 '17 at 20:51
  • It should be noted that accepted answer is a hack, and its necessity indicates incorrect use of JavaScript prototypal inheritance. Because JS isn't C++ or Java. Still, prototypal inheritance is great when done right. – Estus Flask Oct 05 '17 at 10:05

2 Answers2

3

You could do:

Parent.prototype.init.call(this);

class Parent {
  init() {
    console.log("Parent init");
    this._surname = "McClass";
  }
  constructor() {
    console.log("Parent constructor");
    Parent.prototype.init.call(this);
  }

  get surname() {
    return this._surname;
  }
}

class Child extends Parent {

  init() {
    console.log("Child init");
  }
  constructor() {
    super();
    console.log("Child constructor");
    this.init();
  }
}

var child = new Child();

To make sure it never gets overridden, but I would suggest you just not override it in the first place.

MinusFour
  • 13,913
  • 3
  • 30
  • 39
  • Looks like a possibility for my own code - may accept answer later ( things to consider ) . – Bob Oct 04 '17 at 17:28
  • Accepted as it answers the "how". ( Doubt I'll ever get a reasonable explanation as to the "why" ) Cheers ! – Bob Oct 04 '17 at 18:39
  • @Bob, "why" would be a design question that only TC39 could answer. My best bet would be that they didn't want different behavior from what it existed at the moment. – MinusFour Oct 04 '17 at 18:45
  • 1
    @Bob Why? Because JS is highly dynamic, and *every* method call/property access is `virtual`. – Bergi Oct 04 '17 at 18:47
  • @Bergi The client object has not been constructed. Its methods should not be available to the parent. (@ MinusFour waiting on support to sort out the accept button on the answer) – Bob Oct 04 '17 at 18:50
  • @Bob I don't know C++ that well, but in JS the object *starts* as an instance of the child class, and only then are the `.constructor` "methods" run on it (OK, with ES6 `class` syntax the semantics changed a bit and forces the `super` call to be the first thing but that concept remains) – Bergi Oct 04 '17 at 18:57
  • @Bergi. Doing inheritance and js oo without "class" is fine - you know what you're dealing with but then they add "class" and it doesn't work the way you would expect - particularly for people who are not doing old style js oo because they now have class. Given what we've discussed here you cannot safely subclass a class you do not know intimately - the chances of 2 devs moving init out of the constructor and calling it init at some point are huge. Having to know the classes intimately negates the main purpose of classes. – Bob Oct 04 '17 at 19:03
  • 1
    @Bob, without `class` you would still run into [the same problem](https://repl.it/MG1P). So if the constructor based approach is fine according to you then really `class` should also be fine. Unless you were expecting a different inheritance model with the `class` keyword. – MinusFour Oct 04 '17 at 19:34
  • @MinusFour, I was still expecting a js Function object with the usual properties and prototype object model. However, I was expecting class to provide some of the behaviours and conveniences of more traditional/mainstream oo without having to code them eg. the initialisation behaviour I expected in the posting. And yes, I suppose it never said anywhere that it did that, but then if I told you to expect me and my dog for the weekend and I turned up with a pig named "dog" ....... – Bob Oct 04 '17 at 20:49
  • @Bob the initialisation should happen in the constructor anyway, not in an `init` method. That avoids this problem entirely. And it's [not different in C++](https://stackoverflow.com/q/962132/1048572). – Bergi Oct 04 '17 at 21:13
0

It is expected behaviour, just because it can be seen in established ES6 class implementations that follow the specs.

this refers to current class instance, which is an instance of Child in the case when Child is instantiated - even in Parent class, because there is only one instance, and it is instanceof Child.

If Child overrides the method, it's its responsibility to provide mechanism to call it. Considering that init follows some documented convention and is the place where class initialization happens in order to make constructor leaner and more testable, it is:

class Parent {
    init(){...}
    constructor() {
        this.init();
    }
    ...
}

...

class Child extends Parent {
    init(){
        super.init();
        ...
    }
    // optional, as long as `init` contains all init logic
    constructor() {
        super();
    }    
}

Which results in a sequence:

Parent constructor
Parent init
Child init
Child constructor

If init is supposed to work totally independently in both classes, it shouldn't be overridden. Methods should be named differently, like initParent and initChild. Or any other way to avoid naming collisions can be used, e.g.:

const init = Symbol('Parent init');
class Parent {
    [init](){...}
    constructor() {
        this[init]();
    }
    ...
}

...

const init = Symbol('Child init');
class Child extends Parent {
    [init](){...}
    constructor() {
        this[init](); // totally independent method
    }
}
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • basic oo principles - the child should not need to know about its parent's internals. – Bob Oct 04 '17 at 19:09
  • @Bob It doesn't need. A developer does. If you follow some convention on init method, that's how it should be done. You should have opportunity to either override or augment parent `init` in child. It's hard to say if it's the best solution possible without knowing the context for this code. – Estus Flask Oct 04 '17 at 19:16
  • when I say "the child should not need to know about its parent's internals" I mean it should not need references to those internals i.e. the developer writing the child should not need to know about them - there should be no need for a convention on the internals of the parent class. The only thing a consumer of a class (developer or child class) should need to know about is the parent's interface. – Bob Oct 04 '17 at 19:33
  • The developer should know about them in this case. Here `init` is a sort of a hook that follows the convention, and the way it works should be documented. I've used this recipe before, and it really adds testability and some flexibility to class construction. If Parent and Child `init` should be called totally independently, this means that the method shouldn't be overridden at all. There should be different `initParent` and `initChild` methods (or any other way to avoid method name collisions should be used). – Estus Flask Oct 04 '17 at 19:45
  • Even if this principle (though principles aren't carved in stone because they always result from practical considerations) worked for you in C++, this doesn't mean that it applies to JS - because apparently it doesn't and results in `Parent.prototype.init` hack that was shown in another answer. I updated the answer to cover unrelated 'init' methods. – Estus Flask Oct 04 '17 at 19:56