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:
static get length()
to all extends (not my idea of
inheritance)static get size()
works as intended, but is not a generally used property in JS)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)
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
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
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