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:
staticMethod
will become a member of the constructor function object, whereasnonStaticMethod
will become a member of that function object's prototype:If you want to run
staticMethod
from aFoo
'instance' you'll have to navigate to itsconstructor
first, which is the function object wherestaticMethod
is a member of:function.length
All functions have a
length
property that tells you how many arguments that function accepts:So in your example, a prototype lookup for
Bar.length
toFoo.length
will indeed never occur, sincelength
is 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
value
getter you define, you can instead just usefunction.name
to get the name of a function / class constructor:Let's use this to override the
length
property onFoo
. We are still able to overrideFoo.length
because 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
length
property and set avalue
property. Both are accessors with aget
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:We can decorate all classes at once:
If we access
value
andlength
on all three classes (functions) we get the desired results: