I offer a function to allow classes to be defined with multiple inheritance. It allows for code like the following:
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
to produce output like this:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Here are what the class definitions look like:
let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));
let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));
let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));
We can see that each class definition using the makeClass function accepts an Object of parent-class names mapped to parent-classes. It also accepts a function that returns an Object containing properties for the class being defined. This function has a parameter protos, which contains enough information to access any property defined by any of the parent-classes.
The final piece required is the makeClass function itself, which does quite a bit of work. Here it is, along with the rest of the code. I've commented makeClass quite heavily:
let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
  
  // The constructor just curries to a Function named "init"
  let Class = function(...args) { this.init(...args); };
  
  // This allows instances to be named properly in the terminal
  Object.defineProperty(Class, 'name', { value: name });
  
  // Tracking parents of `Class` allows for inheritance queries later
  Class.parents = parents;
  
  // Initialize prototype
  Class.prototype = Object.create(null);
  
  // Collect all parent-class prototypes. `Object.getOwnPropertyNames`
  // will get us the best results. Finally, we'll be able to reference
  // a property like "usefulMethod" of Class "ParentClass3" with:
  // `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  for (let parName in parents) {
    let proto = parents[parName].prototype;
    parProtos[parName] = {};
    for (let k of Object.getOwnPropertyNames(proto)) {
      parProtos[parName][k] = proto[k];
    }
  }
  
  // Resolve `properties` as the result of calling `propertiesFn`. Pass
  // `parProtos`, so a child-class can access parent-class methods, and
  // pass `Class` so methods of the child-class have a reference to it
  let properties = propertiesFn(parProtos, Class);
  properties.constructor = Class; // Ensure "constructor" prop exists
  
  // If two parent-classes define a property under the same name, we
  // have a "collision". In cases of collisions, the child-class *must*
  // define a method (and within that method it can decide how to call
  // the parent-class methods of the same name). For every named
  // property of every parent-class, we'll track a `Set` containing all
  // the methods that fall under that name. Any `Set` of size greater
  // than one indicates a collision.
  let propsByName = {}; // Will map property names to `Set`s
  for (let parName in parProtos) {
    
    for (let propName in parProtos[parName]) {
      
      // Now track the property `parProtos[parName][propName]` under the
      // label of `propName`
      if (!propsByName.hasOwnProperty(propName))
        propsByName[propName] = new Set();
      propsByName[propName].add(parProtos[parName][propName]);
      
    }
    
  }
  
  // For all methods defined by the child-class, create or replace the
  // entry in `propsByName` with a Set containing a single item; the
  // child-class' property at that property name (this also guarantees
  // there is no collision at this property name). Note property names
  // prefixed with "$" will be considered class properties (and the "$"
  // will be removed).
  for (let propName in properties) {
    if (propName[0] === '$') {
      
      // The "$" indicates a class property; attach to `Class`:
      Class[propName.slice(1)] = properties[propName];
      
    } else {
      
      // No "$" indicates an instance property; attach to `propsByName`:
      propsByName[propName] = new Set([ properties[propName] ]);
      
    }
  }
  
  // Ensure that "init" is defined by a parent-class or by the child:
  if (!propsByName.hasOwnProperty('init'))
    throw Error(`Class "${name}" is missing an "init" method`);
  
  // For each property name in `propsByName`, ensure that there is no
  // collision at that property name, and if there isn't, attach it to
  // the prototype! `Object.defineProperty` can ensure that prototype
  // properties won't appear during iteration with `in` keyword:
  for (let propName in propsByName) {
    let propsAtName = propsByName[propName];
    if (propsAtName.size > 1)
      throw new Error(`Class "${name}" has conflict at "${propName}"`);
    
    Object.defineProperty(Class.prototype, propName, {
      enumerable: false,
      writable: true,
      value: propsAtName.values().next().value // Get 1st item in Set
    });
  }
  
  return Class;
};
let Named = makeClass('Named', {}, () => ({
  init: function({ name }) {
    this.name = name;
  }
}));
let Running = makeClass('Running', { Named }, protos => ({
  init: function({ name, numLegs }) {
    protos.Named.init.call(this, { name });
    this.numLegs = numLegs;
  },
  run: function() {
    console.log(`${this.name} runs with ${this.numLegs} legs.`);
  }
}));
let Flying = makeClass('Flying', { Named }, protos => ({
  init: function({ name, numWings }) {
    protos.Named.init.call(this, { name });
    this.numWings = numWings;
  },
  fly: function( ){
    console.log(`${this.name} flies away with ${this.numWings} wings!`);
  }
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
  init: function({ name, numLegs, numWings }) {
    protos.Running.init.call(this, { name, numLegs });
    protos.Flying.init.call(this, { name, numWings });
  },
  takeFlight: function() {
    this.run();
    this.fly();
  }
}));
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
 
 
The makeClass function also supports class properties; these are defined by prefixing property names with the $ symbol (note that the final property name that results will have the $ removed). With this in mind, we could write a specialized Dragon class that models the "type" of the Dragon, where the list of available Dragon types is stored on the Class itself, as opposed to on the instances:
let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({
  
  $types: {
    wyvern: 'wyvern',
    drake: 'drake',
    hydra: 'hydra'
  },
  
  init: function({ name, numLegs, numWings, type }) {
    protos.RunningFlying.init.call(this, { name, numLegs, numWings });
    this.type = type;
  },
  description: function() {
    return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
  }
}));
let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });
The Challenges of Multiple Inheritance
Anyone who followed the code for makeClass closely will note a rather significant undesirable phenomenon occurring silently when the above code runs: instantiating a RunningFlying will result in TWO calls to the Named constructor!
This is because the inheritance graph looks like this:
 (^^ More Specialized ^^)
      RunningFlying
         /     \
        /       \
    Running   Flying
         \     /
          \   /
          Named
  (vv More Abstract vv)
When there are multiple paths to the same parent-class in a sub-class' inheritance graph, instantiations of the sub-class will invoke that parent-class' constructor multiple times.
Combatting this is non-trivial. Let's look at some examples with simplified classnames. We'll consider class A, the most abstract parent-class, classes B and C, which both inherit from A, and class BC which inherits from B and C (and hence conceptually "double-inherits" from A):
let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, protos => ({
  init: function() {
    protos.A.init.call(this);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, protos => ({
  init: function() {
    // Overall "Construct A" is logged twice:
    protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
    protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
    console.log('Construct BC');
  }
}));
If we want to prevent BC from double-invoking A.prototype.init we may need to abandon the style of directly calling inherited constructors. We will need some level of indirection to check whether duplicate calls are occurring, and short-circuit before they happen.
We could consider changing the parameters supplied to the properties function: alongside protos, an Object containing raw data describing inherited properties, we could also include a utility function for calling an instance method in such a way that parent methods are also called, but duplicate calls are detected and prevented. Let's take a look at where we establish the parameters for the propertiesFn Function:
let makeClass = (name, parents, propertiesFn) => {
  
  /* ... a bunch of makeClass logic ... */
  
  // Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
  let parProtos = {};
  /* ... collect all parent methods in `parProtos` ... */
  // Utility functions for calling inherited methods:
  let util = {};
  util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {
    
    // Invoke every parent method of name `fnName` first...
    for (let parName of parProtos) {
      if (parProtos[parName].hasOwnProperty(fnName)) {
        // Our parent named `parName` defines the function named `fnName`
        let fn = parProtos[parName][fnName];
        
        // Check if this function has already been encountered.
        // This solves our duplicate-invocation problem!!
        if (dups.has(fn)) continue;
        dups.add(fn);
        
        // This is the first time this Function has been encountered.
        // Call it on `instance`, with the desired args. Make sure we
        // include `dups`, so that if the parent method invokes further
        // inherited methods we don't lose track of what functions have
        // have already been called.
        fn.call(instance, ...args, dups);
      }
    }
    
  };
  
  // Now we can call `propertiesFn` with an additional `util` param:
  // Resolve `properties` as the result of calling `propertiesFn`:
  let properties = propertiesFn(parProtos, util, Class);
  
  /* ... a bunch more makeClass logic ... */
  
};
The whole purpose of the above change to makeClass is so that we have an additional argument supplied to our propertiesFn when we invoke makeClass. We should also be aware that every function defined in any class may now receive a parameter after all its others, named dup, which is a Set that holds all functions that have already been called as a result of calling the inherited method:
let A = makeClass('A', {}, () => ({
  init: function() {
    console.log('Construct A');
  }
}));
let B = makeClass('B', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct B');
  }
}));
let C = makeClass('C', { A }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct C');
  }
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
  init: function(dups) {
    util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
    console.log('Construct BC');
  }
}));
This new style actually succeeds in ensuring "Construct A" is only logged once when an instance of BC is initialized. But there are three downsides, the third of which is very critical:
- This code has become less readable and maintainable. A lot of complexity hides behind the util.invokeNoDuplicatesfunction, and thinking about how this style avoids multi-invocation is non-intuitive and headache inducing. We also have that peskydupsparameter, which really needs to be defined on every single function in the class. Ouch.
- This code is slower - quite a bit more indirection and computation is required to achieve desirable results with multiple inheritance. Unfortunately this is likely to be the case with any solution to our multiple-invocation problem.
- Most significantly, the structure of functions which rely on inheritance has become very rigid. If a sub-class NiftyClassoverrides a functionniftyFunction, and usesutil.invokeNoDuplicates(this, 'niftyFunction', ...)to run it without duplicate-invocation,NiftyClass.prototype.niftyFunctionwill call the function namedniftyFunctionof every parent class that defines it, ignore any return values from those classes, and finally perform the specialized logic ofNiftyClass.prototype.niftyFunction. This is the only possible structure. IfNiftyClassinheritsCoolClassandGoodClass, and both these parent-classes provideniftyFunctiondefinitions of their own,NiftyClass.prototype.niftyFunctionwill never (without risking multiple-invocation) be able to:
- A. Run the specialized logic of NiftyClassfirst, then the specialized logic of parent-classes
- B. Run the specialized logic of NiftyClassat any point other than after all specialized parent logic has completed
- C. Behave conditionally depending on the return values of its parent's specialized logic
- D. Avoid running a particular parent's specialized niftyFunctionaltogether
Of course, we could solve each lettered problem above by defining specialized functions under util:
- A. define util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)(WhereparentNameis the name of the parent whose specialized logic will be immediately followed by the child-classes' specialized logic)
- C.  define util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(In this casetestFnwould receive the result of the specialized logic for the parent namedparentName, and would return atrue/falsevalue indicating whether the short-circuit should happen)
- D. define util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(In this caseblackListwould be anArrayof parent names whose specialized logic should be skipped altogether)
These solutions are all available, but this is total mayhem! For every unique structure that an inherited function call can take, we would need a specialized method defined under util. What an absolute disaster.
With this in mind we can start to see the challenges of implementing good multiple inheritance. The full implementation of makeClass I provided in this answer does not even consider the multiple-invocation problem, or many other problems which arise regarding multiple inheritance.
This answer is getting very long. I hope the makeClass implementation I included is still useful, even if it isn't perfect. I also hope anyone interested in this topic has gained more context to keep in mind as they do further reading!