Can't mix-in ES6 class expressions
22 Dec 2020
Here's a quick JavaScript bug story. I'm working on Infinite Scroll v4, updating older ES5 code to ES2018.
If you create a class using the traditional function
class definition, you can use mix it in to another class' prototype with Object.assign()
. I use this pattern with EvEmitter and my plugins.
// function class definition
function EvEmitter() {}
// prototype methods
EvEmitter.prototype.emit = function() {};
EvEmitter.prototype.on = function() {};
EvEmitter.prototype.off = function() {};
// plugin function class
function InfiniteScroll() {}
// mixin EvEmitter prototype into Infinite Scroll
Object.assign( InfiniteScroll.prototype, EvEmitter.prototype );
// now InfiniteScroll can use EvEmitter methods
InfiniteScroll.prototype.create = function() {
this.emit( 'load', function() {} );
this.on( 'request', function() {} );
};
But, if you use ES6 classes expressions, you can no longer use the Object.assign()
mix-in pattern.
// class expression
class EvEmitter {
// prototype methods
emit() {}
on() {}
off() {}
}
function InfiniteScroll() {}
// mixin
Object.assign( InfiniteScroll.prototype, EvEmitter.prototype );
InfiniteScroll.prototype.create = function() {
// Uncaught TypeError: this.emit is not a function
this.emit( 'load', function() {} );
this.on( 'request', function() {} );
};
Whaaa? The core issue is that class methods are non-enumerable. If you try iterating over Class.prototype
that was set with a class expression, you'll get nothing.
class EvEmitter {
emit() {}
on() {}
off() {}
}
console.log( Object.keys( EvEmitter.prototype ) );
// => []
// empty!
I suppose this is an improvement
That’s good, because if we for..in over an object, we usually don’t want its class methods.
Except I've been iterating over prototype
for years.
The obvious solution is to define the inherited class with a class expression as well, using extend
to inherit the superclass.
// okay, this works
class InfiniteScroll extends EvEmitter {
create() {
this.emit( 'load', function() {} );
this.on( 'request', function() {} );
}
}
But it's a bummer that I lose a feature by opting-in to the new syntax. EvEmitter
really is a mix-in and I would like to be able to use it like one. Read Angus Croll about why mixins are a good JavaScript pattern.. There is an ES6 approach for mix-ins and class expressions, but I'm not a fan.
So I reverted using class
and switched back to the original function expression & prototype
setting.
function EvEmitter() {}
EvEmitter.prototype.emit = function() {};
EvEmitter.prototype.on = function() {};
EvEmitter.prototype.off = function() {};
YAY back in business.