Skip to content

Instantly share code, notes, and snippets.

@tburette
Created August 12, 2022 11:19
Show Gist options
  • Save tburette/3df109f81910b99a511d2e91ab83d20b to your computer and use it in GitHub Desktop.
Save tburette/3df109f81910b99a511d2e91ab83d20b to your computer and use it in GitHub Desktop.
System to create and inject mixins that work with instanceof and accessor properties
"use strict";
/**
* Make a mixin. Better than basic mixins.
*
* Basic mixin:
* let SomeMixin = {
* value: 0
* }
* Object.assign(Someclass.prototype, SomeMixin)
*
* Problems with basic mixins:
* - instanceof SomeMixin doesn't work
* - accessor properties (get x(){}) are not copied
* instead, the value returned by the accessor is copied
*
* These problems do not exist with this mixin system.
*
* Usage:
* let AMixin = createMixin({
* method() {
* return 456;
* },
* get accessor() {
* return 789;
* },
* });
*
* AMixin.injectInto(MyClass);
*
* let myInstance = new MyClass();
* myInstance.method();
* console.log(myInstance instanceof AMixin); // true
* console.log(myInstance.accessor); // calls 'get accessor()'
*
* @param {object} mixin an object whose fields will be injected
* @param {string} name optionally, the name of the mixin
* @returns
*/
function createMixin(mixin, name=undefined) {
// from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#description
// Like Object.assign but copies accessors instead of
// copying the value returned by the accessor
function completeAssign(target, ...sources) {
sources.forEach((source) => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// By default, Object.assign copies enumerable Symbols, too
Object.getOwnPropertySymbols(source).forEach((sym) => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
}
// The function that will be returned
function Mixin() {}
// if we have a name, set the correct mixin name for the function
// so that TheMixin.name is correct
if(name != undefined) {
Object.defineProperty(Mixin, "name", {
value: name,
configurable: true
});
}
// we put all the mixin content in the function directly and not
// Mixin.prototype.
// Do this so that the user can do : Object.assign(whatever, aMixin)
// instead of Object.assign(whatever, aMixin.prototype)
completeAssign(Mixin.prototype, mixin);
// will be used for hasInstance
let symbol = Symbol.for(name ?? "mixin");
Object.defineProperty(Mixin.prototype, symbol, {
value: symbol,
configurable: false,
enumerable: true,
writable: false,
})
// makes : "anObjectOfAClassWithTheMixin instanceof TheMixin" works
Object.defineProperty(Mixin, Symbol.hasInstance, {
value(obj) {
return obj[symbol] == symbol;
},
});
// to be used when adding a mixin to a class
Mixin.injectInto = function(classOrConstructor) {
completeAssign(classOrConstructor.prototype, Mixin.prototype)
}
return Mixin;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment