ES6 `static get length()` is not inherited as expected

682 Views Asked by At

I'm having an issue with providing a static getter function for the length property of my ES6 class extends. As it turns out the actual Function.length getter always takes precedence over my own implementation.

class Foo {
  static get value() {
    return 'Foo';
  }

  static get length() {
    return this.value.length;
  }
}

class Bar extends Foo {
  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

In the example above, Foo does exactly what I expected it to do, Bar not so much. Bar.value does indeed return 'FooBar', but Bar.length being 0 surprised me.

It took me a while to realize where the 0 came from, as I fully expected it to be 6 (and would have understood 3 to some degree). As it turns out the 0 value provided by Bar.length is in fact the length of the constructor function of Bar, I realised this when wrote the same example in ES5 notation, there is a quick way to prove this though; simply add a constructor to Bar.

class Bar extends Foo {
  constructor(a, b, c, d) {
    //  four configured parameters
  }

  static get value() {
    return `${super.value}Bar`;
  }
}

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 4

There are ways around this:

  • add the static get length() to all extends (not my idea of inheritance)
  • use a different property name (e.g. static get size() works as intended, but is not a generally used property in JS)
  • extend the base from a built-in class which has a working length (e.g. class Foo extends Array {...}) -

None of these are what I want to do if there's a more appropriate way to do this.

So my question is; does anyone know a proper way to have a custom property override which is inherited as expected, or am I being too stubborn?


As mentioned, I figured out what went wrong by writing the class syntax to (what I believe) would be the ES5 equivalent, as it may be beneficial to other developers and may shed some light on how I think ES6 classes work I'll leave it here. (If anyone has a tip on how to make this bit collapsable on Stackoverflow, feel free to edit/suggest)

What I suppose is happening in ES5 syntax

I am aware ES6 classes are mostly syntactic sugar around the prototypal inheritance JS has, so what seems to happen for Bar is something like;

function Foo() {}
Object.defineProperties(Foo, {
  value: {
    configurable: true, 
    get: function() {
      return 'Foo';
    }
  },
  length: {
    configurable: true, 
    get: function() {
      return this.value.length;
    }
  }
});

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, {
  value: { 
    configurable: true, 
    get: function() { 
      return 'Bar' + Foo.value; 
    }
  }
});

console.log(Foo.value, Foo.length);  //  'Foo', 3
console.log(Bar.value, Bar.length);  //  'FooBar', 0

I would've expected the property descriptors of Foo to be taken into account, like:

function Bar() {}
Bar.prototype = Object.create(Object.getPrototypeOf(Foo));
Object.defineProperties(Bar, Object.assign(
  //  inherit any custom descriptors
  Object.getOwnPropertyDescriptors(Foo),
  {
    configurable: true, 
    value: { 
      get: function() { 
        return 'Bar' + Foo.value;
      }
    }
  }
));


console.log(Foo.value, Foo.length);  //  'foo', 3
console.log(Bar.value, Bar.length);  //  'bar', 6
1

There are 1 best solutions below

8
On BEST ANSWER

About static members

Static members of an ES6 class are in fact members of the function object rather than its prototype object. Consider the following example, where I'll use regular methods instead of a getter, but the mechanics are identical to getters:

class Foo {
    static staticMethod() {
        return 'Foo static';
    }

    nonStaticMethod() {
        return 'Foo non-static';
    }
}

staticMethod will become a member of the constructor function object, whereas nonStaticMethod will become a member of that function object's prototype:

function Foo() {}

Foo.staticMethod = function() {
    return 'Foo static';
}

Foo.prototype.nonStaticMethod = function() {
    return 'Foo non-static';
}

If you want to run staticMethod from a Foo 'instance' you'll have to navigate to its constructor first, which is the function object where staticMethod is a member of:

let foo = new Foo();

foo.staticMethod(); // Uncaught TypeError: foo.staticMethod is not a function

// Get the static member either on the class directly
// (this works IF you know that Foo is foo's constructor)

Foo.staticMethod(); // > 'Foo static'


// this is the same, provided that neither 'prototype' nor
// 'prototype.constructor' has been overridden afterwards:

Foo.prototype.constructor.staticMethod(); // > 'Foo static'


// ...or by getting the prototype of foo
// (If you have to perform a computed lookup of an object's constructor)
// You'll want to perform such statements in a try catch though...

Object.getPrototypeOf(foo).constructor.staticMethod(); // > 'Foo static'

function.length

All functions have a length property that tells you how many arguments that function accepts:

function Foo(a, b) {}
Foo.length; // > 2

So in your example, a prototype lookup for Bar.length to Foo.length will indeed never occur, since length is already found directly on Bar. A simple override will not work:

Foo.length = 3;
Foo.length; // still 2

That is because the property is non-writable. Let's verify with getOwnPropertyDescriptor:

Object.getOwnPropertyDescriptor(Foo, 'length');
/*
    { 
        value: 0,
        writable: false,
        enumerable: false,
        configurable: true
    }
*/

Also, instead of the value getter you define, you can instead just use function.name to get the name of a function / class constructor:

Foo.name; // > 'Foo'

Let's use this to override the length property on Foo. We are still able to override Foo.length because the property is configurable:

Object.defineProperty(Foo, 'length', {
    get() {
        return this.name.length;
    }
});

Foo.length; // 3

This is code bloat

It is highly undesirable to have to do this for each extending class, or define a static getter for each, which is equivalent to the code above. It is not possible to entirely override the behaviour without any decoration of the function objects of some sort. But since we know that classes are just syntactic sugar, and we are actually just dealing with objects and functions, writing a decorator is easy!

function decorateClasses(...subjects) {

    subjects.forEach(function(subject) {

        Object.defineProperty(subject, 'value', {
            get() {
                const superValue = Object.getPrototypeOf(this).value || '';

                return superValue + this.name;
            },
            enumerable: true,
            configurable: true,
        });

        Object.defineProperty(subject, 'length', {
            get() {
                return this.value.length;
            },
            enumerable: true,
            configurable: true,
        });
    })

}

This function accepts one or multiple objects, on which it will override the length property and set a value property. Both are accessors with a get method. get value explicitly performs prototype lookups, and then combines the results with the name of the function it belongs to. So, if we have 3 classes:

class Foo {

}

class Bar extends Foo {

}

class Baz extends Bar {

}

We can decorate all classes at once:

decorateClasses(Foo, Bar, Baz);

If we access value and length on all three classes (functions) we get the desired results:

Foo.value; // 'Foo'
Foo.length; // 3

Bar.value; // 'FooBar'
Bar.length; // 6

Baz.value; // 'FooBarBaz'
Baz.length; // 9