Why the getters or the setters are not inherited independently in JavaScript?

Viewed 1230

Consider the following:

class Base {
   _value;
   constructor() {
      this._value = 1;
   }
   get value()  { return this._value; }
   set value(v) { this._value = v;    }
}
class Derived extends Base {
   set value(v) {
      // ...
      super.value = v;
   }
}
const d = new Derived();
d.value = 2;
console.log(d.value); // <-- undefined

I expected the "getter" method of the Base class to be "inherited" in the class of Derived, and consequently to display the value of 2 instead of undefined. Seems like both of the "getter" or "setter" methods are not inherited independently, but rather are considered as a single entity, collectively. In a sense that if the overrided setter method was not present or it was coupled with a respective getter (specifically declared in the derived class, rather than being inherited) as the following:

get value() { return super.value; }

then, there would be no such issue.

So, why the getters or the setters are not inherited independently, as they're supposed to decouple the concepts of reading and setting the fields?

5 Answers

JavaScript’s class inheritance uses the prototype chain to wire the child Constructor.prototype to the parent Constructor.prototype for delegation. Usually, the super() constructor is also called. Those steps form single-ancestor parent/child hierarchies and create the tightest coupling available in OO design.

I suggest you read a very nice article about Master the JavaScript Interview: What’s the Difference Between Class & Prototypal Inheritance? written by Eric Elliott.

UPDATE

For elaboration; this is the expected behavior since you're adding a new descriptor to Derived.prototype. When you add a descriptor using get or set, there is actually a function created with that name so it will evaluate to undefined if is not set. It becomes like an own property.

Standard ECMA-262

14.3.9 Runtime Semantics: PropertyDefinitionEvaluation

  1. MethodDefinition : set PropertyName ( PropertySetParameterList ) { FunctionBody }
  2. Let propKey be the result of evaluating PropertyName. ReturnIfAbrupt(propKey).
  3. If the function code for this MethodDefinition is strict mode code, let strict be true. Otherwise let strict be false.
  4. Let scope be the running execution context’s LexicalEnvironment.
  5. Let formalParameterList be the production FormalParameters : [empty]
  6. Let closure be FunctionCreate(Method, PropertySetParameterList, FunctionBody, scope, strict).
  7. Perform MakeMethod(closure, object).
  8. Perform SetFunctionName(closure, propKey, "set").
  9. Let desc be the PropertyDescriptor{[[Set]]: closure, [[Enumerable]]: enumerable, [[Configurable]]: true}
  10. Return DefinePropertyOrThrow(object, propKey, desc).

6.2.4.6 CompletePropertyDescriptor ( Desc )

When the abstract operation CompletePropertyDescriptor is called with Property Descriptor Desc the following steps are taken:

  1. ReturnIfAbrupt(Desc).
  2. Assert: Desc is a Property Descriptor
  3. Let like be Record{[[Value]]: undefined, [[Writable]]: false, [[Get]]: undefined, [[Set]]: undefined, [[Enumerable]]: false, [[Configurable]]: false}.
  4. If either IsGenericDescriptor(Desc) or IsDataDescriptor(Desc) is true, then
    • a. If Desc does not have a [[Value]] field, set Desc.[[Value]] to like.[[Value]].
    • b. If Desc does not have a [[Writable]] field, set Desc.[[Writable]] to like.[[Writable]].
  5. Else,
    • a. If Desc does not have a [[Get]] field, set Desc.[[Get]] to like.[[Get]].
    • b. If Desc does not have a [[Set]] field, set Desc.[[Set]] to like.[[Set]].
  6. If Desc does not have an [[Enumerable]] field, set Desc.[[Enumerable]] to like.[[Enumerable]].
  7. If Desc does not have a [[Configurable]] field, set Desc.[[Configurable]] to like.[[Configurable]].
  8. Return Desc.

Also have a look at 6.1.7.2 Object Internal Methods and Internal Slots at Table 5 - Essential Internal Methods and especially GetOwnProperty and DefineOwnProperty.

[[GetOwnProperty]] (propertyKey) → Undefined | Property Descriptor

Return a Property Descriptor for the own property of this object whose key is propertyKey, or undefined if no such property exists.

[[DefineOwnProperty]] (propertyKey, PropertyDescriptor) → Boolean

Create or alter the own property, whose key is propertyKey, to have the state described by PropertyDescriptor. Return true if that property was successfully created/updated or false if the property could not be created or updated.

Acessor descriptors have a getter and a setter. You can't have getters/setters without acessor descriptors. Derived.prototype contains the acessor descriptor for value that you specified in Derived. That's just how JavaScript works.

If you want to inherit the getter, you could change the prototype of Derived manually, like so:

Object.defineProperty(Derived.prototype, "value", {
    get: Object.getOwnPropertyDescriptor(Base.prototype, "value").get,
    set: Object.getOwnPropertyDescriptor(Derived.prototype, "value").set
});

The problem that I see is the use of super. if you instead use super() in the constructor for the child class you will get all the method from the parent, so you don't need a setter for value in the child cuz is inherited

class Base {
   _value;
   constructor() {
      this._value = 1;
   }
   get value()  { return this._value; }
   set value(v) { console.log('value in parent',v), this._value = v;    }
}
class Derived extends Base {
   constructor() {
      super();
  }
}
const d = new Derived();
d.value = 7;
console.log(d.value); // <-- 7

public class Car extends Vehicle {

private int numDoors;
private int numWheels;


public Car(String manufacturer,String model,int maxSpeed,double price,int numWheels
        ,int numDoors)
{
    super(manufacturer,model,maxSpeed,price);
    this.numDoors=numDoors;
    this.numWheels=numWheels;

}

public Car()
{

}


public int getNumDoors()
{
    return numDoors;
}


public void setNumDoors(int numDoors)
{
    this.numDoors = numDoors;
}


public int getNumWheels()
{
    return numWheels;
}


public void setNumWheels(int numWheels)
{
    this.numWheels = numWheels;
}

public String toString()
{
    return ("Number of doors:"+numDoors+"\n"+"Number of wheels:"+numWheels+""
            + "\n"+
    "Manufacturer:"+manufacturer+"\n"+
           "Model:"+model+"\n"+"Maximum Speed:"+maxSpeed+"\n"+"Price in euros:"+price+
           "\n");
}

}

package testvehicle;

public class MotorCycle extends Vehicle { private String seat;

public MotorCycle(String manufacturer,String model,int maxSpeed,double price
        ,String seat)
{
    super( manufacturer, model, maxSpeed, price);
    this.seat=seat;
}


public MotorCycle()
{

}


public String getSeat()
{
    return seat;
}


public void setSeat(String seat)
{
    this.seat = seat;
}


public String toString()
{
    return ("Manufacturer:"+manufacturer+"\n"+
           "Model:"+model+"\n"+"Maximum Speed:"+maxSpeed+"\n"+"Price in euros:"+price+
           "\n"+"Seat type:"+seat+"\n");
}

}

package testvehicle;

public abstract class Vehicle//This class doesn't do something!
{
    protected String manufacturer;
    protected String model;
    protected int maxSpeed;
    protected double price;

    public Vehicle(String manufacturer,String model,int maxSpeed,double price)
    {
        this.manufacturer=manufacturer;
        this.model=model;
        this.maxSpeed=maxSpeed;
        this.price=price;

    }

    public Vehicle()
    {

    }


    public String getManufacturer()
    {
        return manufacturer;
    }


    public void setManufacturer(String manufacturer)
    {
        this.manufacturer = manufacturer;
    }


    public String getModel()
    {
        return model;
    }


    public void setModel(String model)
    {
        this.model = model;
    }


    public int getMaxSpeed()
    {
        return maxSpeed;
    }


    public void setMaxSpeed(int maxSpeed)
    {
        this.maxSpeed = maxSpeed;
    }


    public double getPrice()
    {
        return price;
    }


    public void setPrice(double price)
    {
        this.price = price;
    }



   public String toString()
    {
       return ("Manufacturer:"+manufacturer+"\n"+
               "Model:"+model+"\n"+"Maximum Speed:"+maxSpeed+"\n"+"Price in euros:"+price+
               "\n");
    }



}

package testvehicle;

public class Main
{


    public static void main(String[] args)
    {
        Car C=new Car("Opel","Corsa",220,12000.0,4,5);
        MotorCycle M=new MotorCycle("KTM","DUKE-690",250,9000.0,"Agressive");
        System.out.println(C.toString());
        System.out.println();
        System.out.println(M.toString());

    }


}

I'd like to stress out what others already said: that's the way Javascript works.

Javascript was forged in browsers' wars. It's a chaotic set of features, selected with compromises and backward compatibility in mind, not a carefully designed language.

So asking "why getters and setters work the way they do?" is almost like asking why:

console.log(
  (![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]
)
// === fail

or, if we're talking about OOP, "why this works in JS the way it does?"

That should be enough to answer the question "why?".

But also consider:

  1. How to enumerate setters and getters if you want them to be similar to ordinary functions, which are (normally) enumerable in JS? Especially considering that setter and getter have the same name?

    function A() {}
    A.prototype.f = function() {}
    const obj = new A()
    for(const key in obj) console.log(key, "is own property:", obj.hasOwnProperty(key))

  2. Some folks even say that the whole idea of inheritance is evil. And in the case of getters and setters I, personally, think that this code:

    class B extends A { get x() { ... } }
    

    reads as "class B has a read-only property x" and an explicit setter (should it present) would make this code cleaner.

  3. The OOP itself is not carved in stone. Every language implements it in it's own way.

  4. MDN

    JavaScript classes, introduced in ECMAScript 2015, are primarily syntactical sugar over JavaScript's existing prototype-based inheritance. The class syntax does not introduce a new object-oriented inheritance model to JavaScript.

    And getters/setters existed long before that.

  5. The prototype inheritance back in the days was considered by some as a paradigm in its own right.

  6. https://esdiscuss.org/topic/getter-and-setter-inheritance

    My view is that getters and setters introduce properties and that whatever we do should be appropriate to that model

    I do not really understand what they mean by "that model", probably they neither. But that's their opinions. Correct me if I'm wrong... opinions of people influenced the JS history (???). And I might've missed it, but I saw no articulated reasons they are based on.

Related