skip to Main Content

In the following code, the base class has four properties: a getter, a field, and a couple of methods that prints out the values of the other two.

In the subclasses, the base getter can be overridden by either another getter OR a field. However, the base field can only be overridden by other fields, not a getter.

I was a little surprised by this dichotomy. Am I doing something wrong?

class A {
  get foo() {
    return 'A.foo-as-getter';
  }
  bar = 'A.bar-as-field';
  go() {
    console.log(this.constructor.name, 'go', this.foo, this.bar);
    return this;
  }
  go2() {
    console.log('A.go2', this.foo, this.bar);
  }
}

class B extends A {
  foo = 'B.foo-as-field';
  get bar() {
    return 'B.bar-as-getter';
  }
  set bar(val) {}
  go2() {
    console.log('B.go2', this.foo, this.bar);
  }
}

class C extends A {
  get foo() {
    return 'C.foo-as-getter';
  }
  set foo(val) {}
  bar = 'C.bar-as-field';
  go2() {
    console.log('C.go2', this.foo, this.bar);
  }
}

new A().go().go2();
console.log('----');
new B().go().go2();
console.log('----');
new C().go().go2();

2

Answers


  1. You’re not doing anything wrong; this behavior is consistent with how JavaScript handles class inheritance and property overriding.

    1. Overriding Getters: In JavaScript, getters are just special kinds of properties that run a function when accessed. When you override a getter in a subclass with another getter or a field, you’re essentially replacing the property descriptor of that property in the subclass. This is why you can override a getter with either a field or another getter.

    2. Overriding Fields: Fields are essentially properties initialized in the constructor. When you try to override a field with a getter, it doesn’t work as expected because the field initialization in the constructor will overwrite the getter. This is why fields can only be effectively overridden by other fields.

    The behavior you’re observing is expected due to how getters and fields work in classes.

    1. Getters: When you define a getter in a subclass, it overrides the getter in the base class. This is because getters are part of the object’s prototype, and the subclass prototype chain will point to the base class prototype. So, when you access a property that has a getter, JavaScript will look up the prototype chain and use the getter from the subclass if it exists.

    2. Fields: Fields are not part of the prototype; they are instance variables. When you create a new instance of a subclass, the field in the subclass will shadow the field in the base class. However, you can’t override a field with a getter in the subclass because fields are initialized in the constructor, and they will shadow any getters with the same name.

    Here’s a breakdown of what happens in your code:

    • new A().go().go2(); will print "A.getter" and "A.field" because it’s an instance of the base class.

    • new B().go().go2(); will print "B.getter" and "A.field" for go() and "B.getter" and "B.field" for go2(). The getter is overridden by a field in class B, and the field is not effectively overridden because it’s a getter in B.

    • new C().go().go2(); will print "C.getter" and "C.field" for both go() and go2(). Both getter and field are effectively overridden in class C.

    Some code to better explain this behavior

    class A {
      get getter() {
        return "A.getter";
      }
      field = "A.field";
      go() {
        console.log(this.constructor.name, "A.go()", this.getter, this.field);
        return this;
      }
      go2() {
        console.log(this.constructor.name, "A.go2()", this.getter, this.field);
      }
    }
    
    class B extends A {
      getter = "B.getter (factually field)";
      get field() {
        return "B.field (factually getter)";
      }
      go2() {
        console.log(this.constructor.name, "B.go2()", this.getter, this.field);
      }
    }
    
    class C extends A {
      get getter() {
        return "C.getter";
      }
      field = "C.field";
      go2() {
        console.log(this.constructor.name, "C.go2()", this.getter, this.field);
      }
    }
    
    new A().go().go2();
    console.log("----");
    new B().go().go2();
    console.log("----");
    new C().go().go2();
    

    Result

    "A" "A.go()" "A.getter" "A.field"
    "A" "A.go2()" "A.getter" "A.field"
    "----"
    "B" "A.go()" "B.getter (factually field)" "A.field"
    "B" "B.go2()" "B.getter (factually field)" "A.field"
    "----"
    "C" "A.go()" "C.getter" "C.field"
    "C" "C.go2()" "C.getter" "C.field"
    

    So, you’re not doing anything wrong; this is just how JavaScript handles getters and fields in class inheritance.

    Login or Signup to reply.
  2. The explanation is easy: a getter is defined in a prototype, but a field in an object itself. So if the same field is in an object and its prototype, the prototype isn’t used (JS doesn’t look into it because the property is already found in the object):

    class A{
      get foo(){
        return 'bar';
      }
    }
    
    console.log(Object.getOwnPropertyDescriptor(A.prototype, 'foo'));

    To override a field with a prototype’s getter you should delete this field from the base class’ object in the constructor:

    class A{
      foo = 'bar'
    }
    
    class B{
      constructor(){
        delete this.foo;
      }
      get foo(){
        return 'baz';
      }
    }
    
    console.log(new B().foo)
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search