Since ES6, JavaScript enjoys support for classes and static functions akin to static functions in other object-oriented languages. Unfortunately, JavaScript lacks support for static properties, and recommended solutions on Google fail to take into account inheritance. I ran into this problem when implementing a new Mongoose feature that requires a more robust notion of static properties. Specifically, I need static properties that support inheritance via setting prototype
or via extends
. In this article, I'll describe a pattern for implementing static properties in ES6.
Static Methods and Inheritance
Suppose you have a simple ES6 class with a static method.
class Base {
static foo() {
return 42;
}
}
You can use extends
to create a subclass and still have access to the foo()
function.
class Sub extends Base {}
Sub.foo(); // 42
You can also use static getters and setters to set a static property on the Base
class.
let foo = 42;
class Base {
static get foo() { return foo; }
static set foo(v) { foo = v; }
}
Unfortunately, this pattern has undesirable behavior when you subclass Base
. If you set foo
on a subclass, it will set foo
for the Base
class and all other subclasses.
class Sub extends Base {}
console.log(Base.foo, Sub.foo);
Sub.foo = 43;
// Prints "43, 43". The above set `Base.foo` as well as `Sub.foo`
console.log(Base.foo, Sub.foo);
The problem gets worse if your property is an array or an object. Because of prototypical inheritance, if foo
is an array, every subclass will have a reference to the same copy of the array as shown below.
class Base {
static get foo() { return this._foo; }
static set foo(v) { this._foo = v; }
}
Base.foo = [];
class Sub extends Base {}
console.log(Base.foo, Sub.foo);
Sub.foo.push('foo');
// Both arrays now contain 'foo' because they are the same array!
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // true
So JavaScript supports static getters and setters, but using them with objects or arrays is a footgun. Turns out you can do it with a little help from JavaScript's built-in hasOwnProperty()
function.
Static Properties With Inheritance
The key idea is that a JavaScript class is just another object, so you can distinguish between own properties and inherited properties.
class Base {
static get foo() {
// If `_foo` is inherited or doesn't exist yet, treat it as `undefined`
return this.hasOwnProperty('_foo') ? this._foo : void 0;
}
static set foo(v) { this._foo = v; }
}
Base.foo = [];
class Sub extends Base {}
// Prints "[] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false
Base.foo.push('foo');
// Prints "['foo'] undefined"
console.log(Base.foo, Sub.foo);
console.log(Base.foo === Sub.foo); // false
This pattern is neat with classes, but it also works with pre-ES6 JavaScript inheritance. This is important because Mongoose still uses pre-ES6 style inheritance. In hindsight we should have switched sooner, but this feature is the first time we've seen a clear advantage to using ES6 classes and inheritance over just setting a function's prototype
.
function Base() {}
Object.defineProperty(Base, 'foo', {
get: function() { return this.hasOwnProperty('_foo') ? this._foo : void 0; },
set: function(v) { this._foo = v; }
});
Base.foo = [];
// Pre-ES6 inheritance
function Sub1() {}
Sub1.prototype = Object.create(Base.prototype);
// Static properties were annoying pre-ES6
Object.defineProperty(Sub1, 'foo', Object.getOwnPropertyDescriptor(Base, 'foo'));
// ES6 inheritance
class Sub2 extends Base {}
// Prints "[] undefined"
console.log(Base.foo, Sub1.foo);
// Prints "[] undefined"
console.log(Base.foo, Sub2.foo);
Base.foo.push('foo');
// Prints "['foo'] undefined"
console.log(Base.foo, Sub1.foo);
// Prints "['foo'] undefined"
console.log(Base.foo, Sub2.foo);
Moving On
ES6 classes have a major advantage over old school Sub.prototype = Object.create(Base.prototype)
because extends
copies over static properties and functions. With a little extra work using Object.hasOwnProperty()
, you can create static getters and setters that handle inheritance correctly. Be very careful with static properties in JavaScript: extends
still uses prototypical inheritance under the hood. That means static objects and arrays are shared between all subclasses unless you use the hasOwnProperty()
pattern from this article.