0

I have a base class that looks like this:

class Speaker {
  sayHi() {
    if (this.isLoud) {
      console.log('HI');
    } else {
      console.log('hi');
    }
  }
}

And a subclass that looks like:

class LoudSpeaker extends Speaker {
}
LoudSpeaker.isLoud = true;

... where that last line is an attempt at creating a class variable in ES6. The problem is that when I instantiate LoudSpeaker and call sayHi, the isLoud variable is undefined. Is there anyway of accessing a class variable of a subclass from the parent class?

serverpunk
  • 10,665
  • 15
  • 61
  • 95
  • Did you miss to add `class LoudSpeaker extends Speaker`? – maazadeeb Jan 22 '18 at 02:52
  • Well, you could use the template pattern instead and have a method that subclases can overwrite ? – edpaez Jan 22 '18 at 02:53
  • @MaazSyedAdeeb yes, thanks for pointing that out. I just corrected it. – serverpunk Jan 22 '18 at 02:53
  • 1
    `LoudSpeaker.isLoud` - this is setting the property on the "*class*" object, not an instance. `this.isLoud` is trying to access a property of "the instance". If the method *also* accessed `LoudSpeaker.isLoud` property the code "would work". – user2864740 Jan 22 '18 at 02:54
  • @edpaez There are going to be dozens of subclasses with essentially the same functionality as far as that method is concerned. Imagine `RedLoudSpeaker`, `BlueLoudSpeaker`, etc. I'm trying to avoid rewriting those each time... – serverpunk Jan 22 '18 at 02:55
  • @user2864740 right, I'd like to know if there's a way to access a subclass variable from the parent class. It shouldn't be an instance variable. – serverpunk Jan 22 '18 at 02:56
  • The information in https://stackoverflow.com/questions/31644662/get-parent-class-name-from-child-with-es6 (along with the notes above) should be sufficient to answer the question. Once one has the "parent class", then it's simply accessing said property on *that* "class object" (and not `this`, which is *not* a class in context of method..) – user2864740 Jan 22 '18 at 02:56
  • Possible duplicate of [Get parent class name from child with ES6?](https://stackoverflow.com/questions/31644662/get-parent-class-name-from-child-with-es6) , https://stackoverflow.com/questions/44145467/how-to-know-the-ancestor-classes-of-a-given-class-in-javascript-es6?noredirect=1&lq=1 , https://stackoverflow.com/questions/19279232/will-it-be-possible-to-find-out-an-objects-class-and-module-in-ecmascript-harmo?noredirect=1&lq=1 – user2864740 Jan 22 '18 at 02:58
  • `LoudSpeaker.isLoud = true;` is actually creating a "static" variable. It can be accessed only by referencing `LoudSpeaker.isLoad`, and no other way. Even from within `LoudSpeaker`. – maazadeeb Jan 22 '18 at 02:59
  • @MaazSyedAdeeb That is not correct in JavaScript, as classes are still "prototype-based". `LoudSpeaker` is a name resolving to *an object*. JavaScript != Java. As long as the property is accessed *on the correct object* it will return the expected value. – user2864740 Jan 22 '18 at 03:00
  • @user2864740 I'm aware of that. That's why I tried to mention "static", with quotes. I guess it wasn't clear enough. – maazadeeb Jan 22 '18 at 03:02

1 Answers1

4

this.isLoud is not the same thing as LoudSpeaker.isLoud. The first refers to an instance variable, the second refers to a property of the LoudSpeaker constructor function which other languages would call a class variable or a static property. In Javascript, you do not refer to class variables using this.isLoud.

To override the value of the instance variable isLoud in a subclass, you would set its value in the constructor.

class Speaker {
  constructor() {
      this.isLoud = false;   // set default value
  }
  sayHi() {
    if (this.isLoud) {
      console.log('HI');
    } else {
      console.log('hi');
    }
  }
}

class LoudSpeaker extends Speaker {
   constructor() {
       super();
       this.isLoud = true;    // override to set our own value
   }
}

If you really want the isLoud property to be something that a class sets once and is never set again, then you can set it on the prototype and then you can reference it with this.isLoud from within an instance:

class Speaker {
  sayHi() {
    if (this.isLoud) {
      console.log('HI');
    } else {
      console.log('hi');
    }
  }
}

Speak.prototype.isLoud = false;           // set default value

class LoudSpeaker extends Speaker {
}

LoudSpeaker.prototype.isLoud = true;      // override for this class

This is usually not done in Javascript because if you set this.isLoud = whatever, then it sets an "own" property and the property becomes an instance property that is specific to that instance and that can be really confusing and bug causing. So, this is generally not done. If, however, you initialize the value on the prototype, never set it on the instance so you only read its value with x = this.isLoud, then it does behave as a class variable that is accessible via this.

There is discussion of this topic in the ES6 wiki. See ES6 class variable alternatives for details as this capability is intentionally left out of the ES6 class syntax which is why it's implemented here as either a regular instance variable set in the constructor or a property set directly on the prototype (and not via the class syntax).

Angel Politis
  • 10,955
  • 14
  • 48
  • 66
jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • This does not appear to be what the OP wants: "@user2864740 right, I'd like to know if there's a way to access a subclass variable from the parent class. It shouldn't be an instance variable." – user2864740 Jan 22 '18 at 03:03
  • @user2864740 - Your interpretation of the question is not what the code in the question shows. The code in my answer would work just fine. It allows a subclass to set its own default for an instance variable. – jfriend00 Jan 22 '18 at 03:03
  • I'm trying to avoid making `isLoud` an instance variable because part of my code loops over all the subclasses of `Speaker` and checks that value. If it's an instance variable, then I have to instantiate a dummy just to check it. It feels cleaner to do it on the class itself, especially since the instances will never vary with respect to `isLoud`. – serverpunk Jan 22 '18 at 03:03
  • @jfriend00 Although, I did indeed "get the ask wrong".. – user2864740 Jan 22 '18 at 03:04
  • @Rob - You can't really do that. An instantiated object has instance variables. It doesn't have class variables. A class has class variables. If you really wanted to, you could try to figure out what actual constructor created this class and then try to access it's static class variables, but that's not really the correct model for Javascript. Just make it an instance variable like I show in my answer and life is simple. – jfriend00 Jan 22 '18 at 03:05
  • So I can't do `for (let SpeakerSubclass of [RedLoudSpeaker, BlueLoudSpeaker]) { console.log('isLoud:', SpeakerSubclass.isLoud) }`? – serverpunk Jan 22 '18 at 03:07
  • @Rob - Yes, you can do that, but inside an instantiated object, you can't refer to that same value with `this.isLoud`. You'd have to refer to it as `RedLoudSpeaker.isLoud` or you'd have to somehow get the constructor that made this object and reference `constructor.isLoud`. There is no model I'm aware of in Javascript for "overriding" statics with a subclass. FYI, you can usually (not always) get the constructor, by getting the prototype with `Object.getPrototypeOf()` and then referencing the `.constructor` property on the prototype associated with the current object. – jfriend00 Jan 22 '18 at 03:07
  • @Rob - With a better description of your actual problem, we could probably help you come up with a clean way to solve what the actual problem is, but instead, you asked about a particular solution you were trying and I think we've fully answered the question you asked about that. Maybe you need a new question that describes from scratch the actual problem you're trying to solve. – jfriend00 Jan 22 '18 at 03:09
  • Would you not set a default `isLoad` value in the `Speaker` constructor? I agree that the question is confusing. – 2pha Jan 22 '18 at 03:14
  • 1
    @2pha - Funny you should mention that because I just finished an edit to add that before I saw your comment. Yes, there would usually be a default value set in the parent constructor. – jfriend00 Jan 22 '18 at 03:15
  • My code is functionally the same as this, just with "workflows" instead of speakers. The workflows have arcane properties and I thought it would be easier to explain with an example everyone can understand. Anyway, I went the route of just making everything instance variables and it's working, although it feels clunky and non-OOP. Oh well, that's ES6 for you I guess... – serverpunk Jan 22 '18 at 03:27
  • @Rob - Well, I think how "OOP" it is or isn't is a subject of opinion. I find it very OOP. The whole idea of an object in Javascript is that you just access it's properties and don't have to care how it was constructed or what "type" of object it is. Call the instance methods and access the instance properties and all the typical OOP inheritance and override features work. It sounds like you have an opinion about how class statics should work that probably came from a different language. That isn't any more right or wrong than the way you do it in Javascript - just different. – jfriend00 Jan 22 '18 at 03:31
  • @Rob - If you really wanted to use class variables, you could do `Object.getPrototypeOf(this).constructor.isLoud` and that would get the class variable for the constructor that made this object. There would be no inheritance (inheritance is only for instances) - you'd have to set it for every class. As long as the object was constructed in a traditional fashion (which is not always required), that would work, but I don't think it's simpler than just using an instance variable. – jfriend00 Jan 22 '18 at 03:33
  • It seems to me that @Rob would benefit on reading up on how javascript classes are just a sugar coating on top of javascripts prototype inheritance. – 2pha Jan 22 '18 at 03:38
  • @2pha - It does indeed help to understand how the prototype works since that's the root of the actual runtime implementation. – jfriend00 Jan 22 '18 at 03:39
  • To my mind it's a question of normalization and convenience. Do all instances of class `X` have the same value for `y`? If so, then why not define `y` on the class itself? That's what the very concept of class variables was meant for, and most classical OOP languages have them. The convenience comes from not having to instantiate the class to reason about it. I'm a JavaScript and Python developer -- this isn't something I'm stubbornly holding onto from Java, if that's what you were thinking. – serverpunk Jan 22 '18 at 03:39
  • javascript is not a "classical OOP language" – 2pha Jan 22 '18 at 03:40
  • I didn't say it was. I thought _ES6 classes_ were a more substantial implementation of OOP. – serverpunk Jan 22 '18 at 03:41
  • 1
    @Rob - Well, that isn't currently a feature of Javascript and probably has partly to do with the fact that its objects are prototype-based. The ES6 class syntax is just a syntax convenience that populates prototypes. The fundamental implementation didn't really change much with the ES6 syntax (though the new syntax is indeed more convenient). – jfriend00 Jan 22 '18 at 03:43
  • 1
    @Rob I can totally relate, it took me ages to understand prototype inheritance coming from a Java classes background. But I did it before javascript classes were around, so probably wasn't as confusing – 2pha Jan 22 '18 at 03:45
  • @Rob - It occurs to me that you could go outside the ES6 syntax and assign `Speaker.prototype.isLoad = false;` right after your class definition as a default value and then assign `Loudspeaker.prototype.isLoud = true;` after its class definition as an override and not set either in the constructor and then you could test `this.isLoud` from an instance. That would perhaps do what you want. Just don't assign to `this.isLoud` as that will probably confuse the heck out of you. One does not typically use data properties (e.g. non-methods) on the prototype, but it can be done. – jfriend00 Jan 22 '18 at 04:45
  • @Rob - And since prototype properties are shared among all instances and are inherited, it might give you what you were originally interested in. – jfriend00 Jan 22 '18 at 04:47
  • 1
    @Rob - I added the prototype idea to my answer. – jfriend00 Jan 22 '18 at 05:45