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
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:
staticMethodwill become a member of the constructor function object, whereasnonStaticMethodwill become a member of that function object's prototype:If you want to run
staticMethodfrom aFoo'instance' you'll have to navigate to itsconstructorfirst, which is the function object wherestaticMethodis a member of:function.lengthAll functions have a
lengthproperty that tells you how many arguments that function accepts:So in your example, a prototype lookup for
Bar.lengthtoFoo.lengthwill indeed never occur, sincelengthis already found directly onBar. A simple override will not work:That is because the property is non-writable. Let's verify with getOwnPropertyDescriptor:
Also, instead of the
valuegetter you define, you can instead just usefunction.nameto get the name of a function / class constructor:Let's use this to override the
lengthproperty onFoo. We are still able to overrideFoo.lengthbecause the property is configurable: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!
This function accepts one or multiple objects, on which it will override the
lengthproperty and set avalueproperty. Both are accessors with agetmethod.get valueexplicitly performs prototype lookups, and then combines the results with the name of the function it belongs to. So, if we have 3 classes:We can decorate all classes at once:
If we access
valueandlengthon all three classes (functions) we get the desired results: