proxymideprecated

Proxy-based multiple inheritance for JavaScript. Without mixins.

Usage no npm install needed!

<script type="module">
  import proxymi from 'https://cdn.skypack.dev/proxymi';
</script>

README

Proxymi · npm version

Proxy-based multiple inheritance for JavaScript. Without mixins.

Proxymi is a library that adds support for dynamic multiple inheritance to JavaScript with a simple syntax. “Dynamic” means that changes to base classes at runtime are reflected immediately in all derived classes just like programmers would expect when working with single prototype inheritance.

Proxymi uses proxies along with other relatively new language features to provide multiple inheritance. Some of these features are not yet well supported in all browsers. As of today, Proxymi runs in current versions of Chrome, Firefox, Safari[notes], Opera and in Node.js. As JavaScript support in other browsers improves, Proxymi will start to run in those browsers, too.

Features

  • C++ style multiple inheritance
  • Works in Node.js and in most browsers
  • Full TypeScript support
  • Zero dependencies
  • Qualified or unqualified access to all base class features
    • constructors
    • methods, getters and setters – both static and nonstatic
    • value properties on base classes and base instance prototypes
  • in, instanceof and isPrototypeOf integration

Setup Instructions

In the Browser

To use Proxymi in your project, download proxymi.js or proxymi.min.js from GitHub and include it in your HTML file.

<script src="proxymi.js"></script>

Alternatively, you can hotlink the current stable version using a CDN of your choice.

<script src="https://gitcdn.xyz/repo/fasttime/Proxymi/master/lib/proxymi.min.js"></script>

In Node.js

If you are using Node.js 8 or later, you can install Proxymi with npm.

npm install proxymi

Then you can use it in your code.

require("proxymi");

Usage

Inherit from more than one base class

For example, declare a derived class ColoredCircle that inherits from both base classes Circle and ColoredObject.

class Circle
{
    constructor(centerX, centerY, radius)
    {
        this.moveTo(centerX, centerY);
        this.radius = radius;
    }
    get diameter() { return this.radius * 2; }
    set diameter(diameter) { this.radius = diameter / 2; }
    moveTo(centerX, centerY)
    {
        this.centerX = centerX;
        this.centerY = centerY;
    }
    toString()
    {
        return `circle with center (${this.centerX}, ${this.centerY}) and radius ${this.radius}`;
    }
}

class ColoredObject
{
    static areSameColor(obj1, obj2) { return obj1.color === obj2.color; }
    constructor(color) { this.color = color; }
    paint() { console.log(`painting in ${this.color}`); }
    toString() { return `${this.color} object`; }
}

class ColoredCircle
extends classes(Circle, ColoredObject) // Base classes in a comma-separated list
{
    // Add methods here.
}

Use methods and accessors from all base classes

const c = new ColoredCircle();

c.moveTo(42, 31);
c.radius = 1;
c.color = 'red';
console.log(c.centerX, c.centerY);  // 42, 31
console.log(c.diameter);            // 2
c.paint();                          // "painting in red"

instanceof works just like it should

const c = new ColoredCircle();

console.log(c instanceof Circle);           // true
console.log(c instanceof ColoredObject);    // true
console.log(c instanceof ColoredCircle);    // true
console.log(c instanceof Object);           // true
console.log(c instanceof Array);            // false

In pure JavaScript, the expression

B.prototype instanceof A

determines if A is a base class of class B.

Proxymi takes care that this test still works well with multiple inheritance.

console.log(ColoredCircle.prototype instanceof Circle);         // true
console.log(ColoredCircle.prototype instanceof ColoredObject);  // true
console.log(ColoredCircle.prototype instanceof ColoredCircle);  // false
console.log(ColoredCircle.prototype instanceof Object);         // true
console.log(Circle.prototype instanceof ColoredObject);         // false

isPrototypeOf works fine, too

const c = new ColoredCircle();

console.log(Circle.prototype.isPrototypeOf(c));         // true
console.log(ColoredObject.prototype.isPrototypeOf(c));  // true
console.log(ColoredCircle.prototype.isPrototypeOf(c));  // true
console.log(Object.prototype.isPrototypeOf(c));         // true
console.log(Array.prototype.isPrototypeOf(c));          // false
console.log(Circle.isPrototypeOf(ColoredCircle));               // true
console.log(ColoredObject.isPrototypeOf(ColoredCircle));        // true
console.log(ColoredCircle.isPrototypeOf(ColoredCircle));        // false
console.log(Object.isPrototypeOf(ColoredCircle));               // false
console.log(Function.prototype.isPrototypeOf(ColoredCircle));   // true

Invoke multiple base constructors

Use arrays to group together parameters for each base constructor in the derived class constructor.

class ColoredCircle
extends classes(Circle, ColoredObject)
{
    constructor(centerX, centerY, radius, color)
    {
        super(
            [centerX, centerY, radius], // Circle constructor params
            [color]                     // ColoredObject constructor params
        );
    }
}

If you prefer to keep parameter lists associated to their base classes explicitly without relying on order, there is an alternative syntax.

class ColoredCircle
extends classes(Circle, ColoredObject)
{
    constructor(centerX, centerY, radius, color)
    {
        super(
            { super: ColoredObject, arguments: [color] },
            { super: Circle, arguments: [centerX, centerY, radius] }
        );
    }
}

There is no need to specify an array of parameters for each base constructor. If the parameter arrays are omitted, the base constructors will still be invoked without parameters.

class ColoredCircle
extends classes(Circle, ColoredObject)
{
    constructor()
    {
        super(); // Base constructors invoked without parameters
        this.centerX    = 0;
        this.centerY    = 0;
        this.radius     = 1;
        this.color      = 'white';
    }
}

Use base class methods and accessors

As usual, the keyword super invokes a base class method or accessor when used inside a derived class.

class ColoredCircle
extends classes(Circle, ColoredObject)
{
    paint()
    {
        console.log("Using method paint of some base class");
        super.paint();
    }
}

If different base classes include a method or accessor with the same name, the syntax

super.class(DirectBaseClass).methodOrAccessor

can be used to make the invocation unambiguous.

class ColoredCircle
extends classes(Circle, ColoredObject)
{
    toString()
    {
        // Using method toString of base class Circle
        const circleString = super.class(Circle).toString();
        return `${circleString} in ${this.color}`;
    }
}

Static methods and accessors are inherited, too

ColoredCircle.areSameColor(c1, c2)

same as

ColoredObject.areSameColor(c1, c2)

Dynamic base class changes

If a property in a base class is added, removed or modified at runtime, the changes are immediately reflected in all derived classes. This is the magic of proxies.

const c = new ColoredCircle();

Circle.prototype.foo = () => console.log("foo");
c.foo(); // print "foo"

Compatibility

Proxymi was successfully tested in the following browsers / JavaScript engines.

  • Chrome 54+
  • Firefox 51+
  • Safari 11 (Partial support. See notes below.)
  • Opera 41+
  • Node.js 8+

Because of poor ECMAScript compliance, Safari accepts non-constructor functions (such as arrow functions, generators, etc.) as arguments to classes,[issue] although it does not allow instantiating classes derived from such functions. Also, Safari does not throw a TypeError when attempting to assign to a read-only property of a Proxymi class or object in strict mode.[issue]

In the current version of Edge, the JavaScript engine Chakra has a serious bug that can produce incorrect results when the instanceof operator is used with bound functions after Proxymi has been loaded. For this reason it is recommended not to use Proxymi in Edge as long as this issue persists.