proxymi
v0.9.0Proxymi ·
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
andisPrototypeOf
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.