UNPKG

mics

Version:

Multiple Inheritance Class System: Intuitive mixins for ES6 classes

152 lines (139 loc) 5.18 kB
export { mix, is, like } /** * Accepts an optional superclass as the first argument, * then a bunch of mixins and an optional class factory as the last argument and returns a mixin. * Mostly, you will be using mix with a factory to create mixins, like this: * * var Looker = mix(superclass => class Looker extends superclass { * constructor() { * super() * console.info('A looker is born!') * } * look() { * console.info('Looking good!') * } * }) * * @param {function} args * Consists of the following three argument groups: * {function} superclass (optional) * {function} mixins... (0 or more) * {function} factory (optional) * @return {function} */ function mix(...args) { // todo: refactor to make const let superclass = !isFactory(args[0]) && args.shift() || baseclass let factory = (isFactory(args[args.length-1]) && args.pop()) || derive superclass = isMixin(superclass) ? superclass.class : derive(superclass) if (args.length) factory = (org => superclass => org(args.reduce((s,m) => m.mixin(s), superclass)))(factory) function mixin(superclass) { const result = is(superclass, mixin) ? superclass : factory(superclass) if (mixin.classes.indexOf(result) === -1) mixin.classes.push(result) return result } Object.defineProperties(mixin, { classes: { value:[], writable:false }, mixins: { value:args, writable:false }, }) const Class = mixin(superclass) const constructor = Class.hasOwnProperty('constructor') ? Class.constructor.bind(Class) : (...args) => new Class(...args) Object.getOwnPropertyNames(Class).forEach(k => Object.defineProperty(constructor, k, { value: Class[k] })) return Object.defineProperties(constructor, { mixin: { value:mixin, writable:false }, class: { value: Class, writable:false }, interface: { get:(x => () => x ? x : x = getInterface(Class.prototype))() }, }) } /** * Tests whether `x` is a type or extends from type. * Example: is(looker, Looker) * * @param {object|function} x * @param {function} type * @return {boolean} */ function is(x, type) { if (typeof x == 'object') { if (x instanceof type) return true if (type.class && x instanceof type.class) return true if (type.mixin && type.mixin.classes) return type.mixin.classes.reduce((f,c) => f || is(x,c), false) } else if (typeof x == 'function') { if (x.mixin && x.mixin.mixins.indexOf(type) !== -1) return true let c = x while (c !== Object) { if (c === type || c === type.class) return true if (type.mixin && type.mixin.classes && type.mixin.classes.indexOf(c) !== -1) return true c = Object.getPrototypeOf(c.prototype).constructor } } return false } /** * Often, we don't really care whether the object is a certain type, * we just want to know whether we can treat it like a certain type. * Use like(subject, type) to test whether a subject adheres to the same interface as is defined by type * Example: * * var Looker = mix(superclass => class Looker extends superclass { * look() {} * }) * * var Viewer = { * look() {} // same interface as Looker * } * * var viewer = new Viewer() * like(viewer, Looker) // true * * @param {object|function} x * @param {function} type * @return {boolean} */ function like(x, type) { if (is(x, type)) return true const itf = type.interface || ((typeof type == 'function') && getInterface(type.prototype)) const subject = typeof x == 'function' ? x.interface || getInterface(x.prototype) : x return itf && Object.keys(itf).reduce((f, k) => f && ((typeof itf[k] == 'function') ? (typeof subject[k] == 'function') : k in subject), true ) } /** * Get all parts of an interface as an array of strings * * @param {object} proto * @return {array<string>} */ function getInterface(proto) { return getPropertyNames(proto).reduce((o,k) => { o[k] = proto[k]; return o }, {}) } /** * Get all properties of an object an an array of strings * * @param {object|function} proto * @return {array<string>} */ function getPropertyNames(proto) { const results = [] while (proto !== Object.prototype) { Object.getOwnPropertyNames(proto).reduce((arr,k) => arr.indexOf(k) === -1 ? arr.push(k) && arr : arr, results) proto = Object.getPrototypeOf(proto).constructor.prototype } return results } function isMixin(x) { return (typeof x == 'function') && !!x.mixin } function isClass(x) { if (typeof x != 'function') return false const s = x.toString() return /^class\s/.test(s) || /^.*classCallCheck\(/.test(s.replace(/^[^{]*{\s*/,'').replace(/\s*}[^}]*$/,'')) } function isFactory(x) { return (typeof x == 'function') && !isMixin(x) && !isClass(x) && x.length == 1 } const baseclass = class Object{} const derive = superclass => ({}[superclass.name || 'Object'] = class extends superclass {})